CORDA-2520: Add FetchParametersFlow (#4674)

* CORDA-2520: Add FetchParametersFlow

* Address comments, add test
This commit is contained in:
Katarzyna Streich 2019-01-29 18:16:37 +00:00 committed by GitHub
parent 88e4b85537
commit 6efd54fce1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 246 additions and 44 deletions

View File

@ -8,12 +8,9 @@ import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.Party
import net.corda.core.internal.BackpressureAwareTimedFlow
import net.corda.core.internal.FetchDataFlow
import net.corda.core.internal.NetworkParametersServiceInternal
import net.corda.core.internal.*
import net.corda.core.internal.notary.generateSignature
import net.corda.core.internal.notary.validateSignatures
import net.corda.core.internal.pushToLoggingContext
import net.corda.core.transactions.*
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData
@ -107,7 +104,7 @@ class NotaryFlow {
check(stx.coreTransaction is NotaryChangeWireTransaction) {
"Notary $notaryParty is not on the network parameter whitelist. A non-whitelisted notary can only be used for notary change transactions"
}
val historicNotary = (serviceHub.networkParametersService as NetworkParametersServiceInternal).getHistoricNotary(notaryParty)
val historicNotary = (serviceHub.networkParametersService as NetworkParametersStorage).getHistoricNotary(notaryParty)
?: throw IllegalStateException("The notary party $notaryParty specified by transaction ${stx.id}, is not recognised as a current or historic notary.")
historicNotary.validating

View File

@ -3,9 +3,7 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.StateAndRef
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.FetchDataFlow
import net.corda.core.internal.RetrieveAnyTransactionPayload
import net.corda.core.internal.readFully
import net.corda.core.internal.*
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.unwrap
@ -42,7 +40,7 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
@Suspendable
override fun call(): Void? {
// The first payload will be the transaction data, subsequent payload will be the transaction/attachment data.
// The first payload will be the transaction data, subsequent payload will be the transaction/attachment/network parameters data.
var payload = payload
// Depending on who called this flow, the type of the initial payload is different.
@ -93,6 +91,10 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
serviceHub.attachments.openAttachment(it)?.open()?.readFully()
?: throw FetchDataFlow.HashNotFound(it)
}
FetchDataFlow.DataType.PARAMETERS -> dataRequest.hashes.map {
(serviceHub.networkParametersService as NetworkParametersStorage).lookupSigned(it)
?: throw FetchDataFlow.MissingNetworkParameters(it)
}
}
}
}

View File

@ -1,6 +1,8 @@
package net.corda.core.internal
import net.corda.core.contracts.NamedByHash
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.crypto.verify
import net.corda.core.serialization.CordaSerializable
@ -43,7 +45,9 @@ Cert path: $fullCertPath
/** Similar to [SignedData] but instead of just attaching the public key, the certificate for the key is attached instead. */
@CordaSerializable
class SignedDataWithCert<T : Any>(val raw: SerializedBytes<T>, val sig: DigitalSignatureWithCert) {
class SignedDataWithCert<T : Any>(val raw: SerializedBytes<T>, val sig: DigitalSignatureWithCert): NamedByHash {
override val id: SecureHash get () = raw.hash
fun verified(): T {
sig.verify(raw)
return uncheckedCast(raw.deserialize<Any>())

View File

@ -10,6 +10,7 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.internal.FetchDataFlow.DownloadedVsRequestedDataMismatch
import net.corda.core.internal.FetchDataFlow.HashNotFound
import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationToken
import net.corda.core.serialization.SerializeAsToken
@ -51,6 +52,8 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
class HashNotFound(val requested: SecureHash) : FlowException()
class MissingNetworkParameters(val requested: SecureHash) : FlowException("Failed to fetch network parameters with hash: $requested")
class IllegalTransactionRequest(val requested: SecureHash) : FlowException("Illegal attempt to request a transaction (${requested}) that is not in the transitive dependency graph of the sent transaction.")
@CordaSerializable
@ -64,11 +67,11 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
@CordaSerializable
enum class DataType {
TRANSACTION, ATTACHMENT
TRANSACTION, ATTACHMENT, PARAMETERS
}
@Suspendable
@Throws(HashNotFound::class)
@Throws(HashNotFound::class, MissingNetworkParameters::class)
override fun call(): Result<T> {
// Load the items we have from disk and figure out which we're missing.
val (fromDisk, toFetch) = loadWhatWeHave()
@ -139,7 +142,7 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
}
/**
* Given a set of hashes either loads from from local storage or requests them from the other peer. Downloaded
* Given a set of hashes either loads from local storage or requests them from the other peer. Downloaded
* attachments are saved to local storage automatically.
*/
class FetchAttachmentsFlow(requests: Set<SecureHash>,
@ -158,10 +161,10 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
} catch (e: FileAlreadyExistsException) {
// This can happen when another transaction will insert the same attachment during this transaction.
// The outcome is the same (the attachment is imported), so we can ignore this exception.
logger.debug("Attachment ${attachment.id} already inserted.")
logger.debug { "Attachment ${attachment.id} already inserted." }
}
} else {
logger.debug("Attachment ${attachment.id} already exists, skipping.")
logger.debug { "Attachment ${attachment.id} already exists, skipping." }
}
}
}
@ -193,3 +196,28 @@ class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: FlowSession) :
override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid)
}
/**
* Given a set of hashes either loads from local network parameters storage or requests them from the other peer. Downloaded
* network parameters are saved to local parameters storage automatically. This flow can be used only if the minimumPlatformVersion is >= 4.
* Nodes on lower versions won't respond to this flow.
*/
class FetchNetworkParametersFlow(requests: Set<SecureHash>,
otherSide: FlowSession) : FetchDataFlow<SignedDataWithCert<NetworkParameters>, SignedDataWithCert<NetworkParameters>>(requests, otherSide, DataType.PARAMETERS) {
override fun load(txid: SecureHash): SignedDataWithCert<NetworkParameters>? {
return (serviceHub.networkParametersService as NetworkParametersStorage).lookupSigned(txid)
}
override fun maybeWriteToDisk(downloaded: List<SignedDataWithCert<NetworkParameters>>) {
for (parameters in downloaded) {
with(serviceHub.networkParametersService as NetworkParametersStorage) {
if (!hasParameters(parameters.id)) {
// This will perform the signature check too and throws SignatureVerificationException
saveParameters(parameters)
} else {
logger.debug { "Network parameters ${parameters.id} already exists in storage, skipping." }
}
}
}
}
}

View File

@ -1,12 +1,44 @@
package net.corda.core.internal
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.node.services.NetworkParametersService
import java.security.cert.X509Certificate
interface NetworkParametersStorage : NetworkParametersService {
/**
* Return parameters epoch for the given parameters hash. Null if there are no parameters for this hash in the storage and we are unable to
* get them from network map.
*/
fun getEpochFromHash(hash: SecureHash): Int?
/**
* Return signed network parameters with certificate for the given hash. Null if there are no parameters for this hash in the storage.
* (No fallback to network map.)
*/
fun lookupSigned(hash: SecureHash): SignedDataWithCert<NetworkParameters>?
/**
* Checks if parameters with given hash are in the storage.
*/
fun hasParameters(hash: SecureHash): Boolean
interface NetworkParametersServiceInternal : NetworkParametersService {
/**
* Returns the [NotaryInfo] for a notary [party] in the current or any historic network parameter whitelist, or null if not found.
*/
fun getHistoricNotary(party: Party): NotaryInfo?
/**
* Save signed network parameters data. Internally network parameters bytes should be stored with the signature.
* It's because of ability of older nodes to function in network where parameters were extended with new fields.
* Hash should always be calculated over the serialized bytes.
*/
fun saveParameters(signedNetworkParameters: SignedDataWithCert<NetworkParameters>)
/**
* Set information that given parameters are current parameters for the network.
*/
fun setCurrentParameters(currentSignedParameters: SignedDataWithCert<NetworkParameters>, trustRoot: X509Certificate)
}

View File

@ -77,6 +77,8 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
@Suspendable
@Throws(FetchDataFlow.HashNotFound::class, FetchDataFlow.IllegalTransactionRequest::class)
override fun call() {
val counterpartyPlatformVersion = serviceHub.networkMapCache.getNodeByLegalIdentity(otherSide.counterparty)?.platformVersion ?:
throw FlowException("Couldn't retrieve party's ${otherSide.counterparty} platform version from NetworkMapCache")
val newTxns = ArrayList<SignedTransaction>(txHashes.size)
// Start fetching data.
for (pageNumber in 0..(txHashes.size - 1) / RESOLUTION_PAGE_SIZE) {
@ -85,6 +87,11 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
newTxns += downloadDependencies(page)
val txsWithMissingAttachments = if (pageNumber == 0) signedTransaction?.let { newTxns + it } ?: newTxns else newTxns
fetchMissingAttachments(txsWithMissingAttachments)
// Fetch missing parameters flow was added in version 4. This check is needed so we don't end up with node V4 sending parameters
// request to node V3 that doesn't know about this protocol.
if (counterpartyPlatformVersion >= 4) {
fetchMissingParameters(txsWithMissingAttachments)
}
}
otherSide.send(FetchDataFlow.Request.End)
// Finish fetching data.
@ -180,4 +187,13 @@ class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
if (missingAttachments.isNotEmpty())
subFlow(FetchAttachmentsFlow(missingAttachments.toSet(), otherSide))
}
// TODO This can also be done in parallel. See comment to [fetchMissingAttachments] above.
@Suspendable
private fun fetchMissingParameters(downloads: List<SignedTransaction>) {
val parameters = downloads.mapNotNull { it.networkParametersHash }
val missingParameters = parameters.filter { !(serviceHub.networkParametersService as NetworkParametersStorage).hasParameters(it) }
if (missingParameters.isNotEmpty())
subFlow(FetchNetworkParametersFlow(missingParameters.toSet(), otherSide))
}
}

View File

@ -0,0 +1,132 @@
package net.corda.core.internal
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.getOrThrow
import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetworkParameters
import net.corda.testing.node.StartedMockNode
import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP
import net.corda.testing.node.internal.cordappForClasses
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
class NetworkParametersResolutionTest {
private lateinit var params2: NetworkParameters
private val certKeyPair: CertificateAndKeyPair = createDevNetworkMapCa()
private lateinit var mockNet: MockNetwork
private lateinit var notaryNode: StartedMockNode
private lateinit var megaCorpNode: StartedMockNode
private lateinit var miniCorpNode: StartedMockNode
private lateinit var megaCorpParty: Party
private lateinit var miniCorpParty: Party
private lateinit var notaryParty: Party
@Before
fun setup() {
mockNet = MockNetwork(MockNetworkParameters(
cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, cordappForClasses(ResolveTransactionsFlowTest.TestFlow::class.java, ResolveTransactionsFlowTest.TestResponseFlow::class.java))))
notaryNode = mockNet.defaultNotaryNode
megaCorpNode = mockNet.createPartyNode(CordaX500Name("MegaCorp", "London", "GB"))
miniCorpNode = mockNet.createPartyNode(CordaX500Name("MiniCorp", "London", "GB"))
notaryParty = mockNet.defaultNotaryIdentity
megaCorpParty = megaCorpNode.info.singleIdentity()
miniCorpParty = miniCorpNode.info.singleIdentity()
params2 = testNetworkParameters(epoch = 2, minimumPlatformVersion = 3, notaries = listOf((NotaryInfo(notaryParty, true))))
}
@After
fun tearDown() {
mockNet.stopNodes()
}
// This function is resolving and signing WireTransaction with special parameters.
private fun TransactionBuilder.toSignedTransactionWithParameters(parameters: NetworkParameters?, services: ServiceHub): SignedTransaction {
val wtx = toWireTransaction(services)
val wtxWithHash = SerializationFactory.defaultFactory.withCurrentContext(null) {
WireTransaction(
createComponentGroups(
wtx.inputs,
wtx.outputs,
wtx.commands,
wtx.attachments,
wtx.notary,
wtx.timeWindow,
wtx.references,
parameters?.serialize()?.hash),
wtx.privacySalt
)
}
val publicKey = services.myInfo.singleIdentity().owningKey
val signatureMetadata = SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(publicKey).schemeNumberID)
val signableData = SignableData(wtxWithHash.id, signatureMetadata)
val sig = services.keyManagementService.sign(signableData, publicKey)
return SignedTransaction(wtxWithHash, listOf(sig))
}
// Similar to ResolveTransactionsFlowTest but creates transactions with given network parameters
// First transaction in pair is dependency of the second one.
private fun makeTransactions(parameters1: NetworkParameters?, parameters2: NetworkParameters?): Pair<SignedTransaction, SignedTransaction> {
// Make a chain of custody of dummy states and insert into node A.
val dummy1: SignedTransaction = DummyContract.generateInitial(0, notaryParty, megaCorpParty.ref(1)).let {
val ptx = it.toSignedTransactionWithParameters(parameters1, megaCorpNode.services)
notaryNode.services.addSignature(ptx, notaryParty.owningKey)
}
val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), miniCorpParty).let {
val ptx = it.toSignedTransactionWithParameters(parameters2, megaCorpNode.services)
notaryNode.services.addSignature(ptx, notaryParty.owningKey)
}
megaCorpNode.transaction {
megaCorpNode.services.recordTransactions(dummy1, dummy2)
// Record parameters too.
with(megaCorpNode.services.networkParametersService as NetworkParametersStorage) {
parameters1?.let { saveParameters(certKeyPair.sign(it)) }
parameters2?.let { saveParameters(certKeyPair.sign(it)) }
}
}
return Pair(dummy1, dummy2)
}
@Test
fun `request parameters that are not in the storage`() {
val params1 = miniCorpNode.services.networkParameters
val hash1 = params1.serialize().hash
val hash2 = params2.serialize().hash
// Create two transactions on megaCorpNode
val (stx1, stx2) = makeTransactions(params1, params2)
assertThat(stx1.networkParametersHash).isEqualTo(hash1)
assertThat(stx2.networkParametersHash).isEqualTo(hash2)
miniCorpNode.transaction {
assertThat(miniCorpNode.services.networkParametersService.lookup(hash1)).isNotNull()
assertThat(miniCorpNode.services.networkParametersService.lookup(hash2)).isNull()
}
// miniCorpNode resolves the stx2 from megaCorpParty
val p = ResolveTransactionsFlowTest.TestFlow(setOf(stx2.id), megaCorpParty)
val future = miniCorpNode.startFlow(p)
mockNet.runNetwork()
future.getOrThrow()
miniCorpNode.transaction {
// Check that parameters were downloaded to the storage.
assertThat(miniCorpNode.services.networkParametersService.lookup(hash1)).isEqualTo(params1)
assertThat(miniCorpNode.services.networkParametersService.lookup(hash2)).isEqualTo(params2)
}
}
}

View File

@ -229,7 +229,7 @@ class ResolveTransactionsFlowTest {
@InitiatingFlow
private open class TestFlow(val otherSide: Party, private val resolveTransactionsFlowFactory: (FlowSession) -> ResolveTransactionsFlow, private val txCountLimit: Int? = null) : FlowLogic<Unit>() {
open class TestFlow(val otherSide: Party, private val resolveTransactionsFlowFactory: (FlowSession) -> ResolveTransactionsFlow, private val txCountLimit: Int? = null) : FlowLogic<Unit>() {
constructor(txHashes: Set<SecureHash>, otherSide: Party, txCountLimit: Int? = null) : this(otherSide, { ResolveTransactionsFlow(txHashes, it) }, txCountLimit = txCountLimit)
constructor(stx: SignedTransaction, otherSide: Party) : this(otherSide, { ResolveTransactionsFlow(stx, it) })
@ -243,7 +243,7 @@ class ResolveTransactionsFlowTest {
}
@Suppress("unused")
@InitiatedBy(TestFlow::class)
private class TestResponseFlow(val otherSideSession: FlowSession) : FlowLogic<Void?>() {
class TestResponseFlow(val otherSideSession: FlowSession) : FlowLogic<Void?>() {
@Suspendable
override fun call() = subFlow(TestNoSecurityDataVendingFlow(otherSideSession))
}

View File

@ -2,10 +2,7 @@ package net.corda.node.internal
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.SignedDataWithCert
import net.corda.core.internal.NetworkParametersServiceInternal
import net.corda.core.internal.*
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.serialization.SerializedBytes
@ -26,23 +23,6 @@ import org.apache.commons.lang.ArrayUtils
import java.security.cert.X509Certificate
import javax.persistence.*
interface NetworkParametersStorage : NetworkParametersServiceInternal {
/**
* Return parameters epoch for the given parameters hash. Null if there are no parameters for this hash in the storage and we are unable to
* get them from network map.
*/
fun getEpochFromHash(hash: SecureHash): Int?
/**
* Save signed network parameters data. Internally network parameters bytes should be stored with the signature.
* It's because of ability of older nodes to function in network where parameters were extended with new fields.
* Hash should always be calculated over the serialized bytes.
*/
fun saveParameters(signedNetworkParameters: SignedDataWithCert<NetworkParameters>)
fun setCurrentParameters(currentSignedParameters: SignedDataWithCert<NetworkParameters>, trustRoot: X509Certificate)
}
class DBNetworkParametersStorage(
cacheFactory: NamedCacheFactory,
private val database: CordaPersistence,
@ -94,6 +74,12 @@ class DBNetworkParametersStorage(
override fun getEpochFromHash(hash: SecureHash): Int? = lookup(hash)?.epoch
override fun lookupSigned(hash: SecureHash): SignedDataWithCert<NetworkParameters>? {
return database.transaction { hashToParameters[hash] }
}
override fun hasParameters(hash: SecureHash): Boolean = hash in hashToParameters
override fun saveParameters(signedNetworkParameters: SignedNetworkParameters) {
log.trace { "Saving new network parameters to network parameters storage." }
val networkParameters = signedNetworkParameters.verified()
@ -104,7 +90,6 @@ class DBNetworkParametersStorage(
}
}
// TODO For the future we could get them also as signed (by network operator) attachments on transactions.
private fun tryDownloadUnknownParameters(parametersHash: SecureHash): NetworkParameters? {
return if (networkMapClient != null) {
try {

View File

@ -13,7 +13,6 @@ import net.corda.core.node.services.KeyManagementService
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.minutes
import net.corda.node.internal.NetworkParametersStorage
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.NetworkParameterAcceptanceSettings
import net.corda.node.utilities.NamedThreadFactory

View File

@ -18,7 +18,6 @@ import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.serialize
import net.corda.core.utilities.millis
import net.corda.node.VersionInfo
import net.corda.node.internal.NetworkParametersStorage
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.NetworkParameterAcceptanceSettings
import net.corda.nodeapi.internal.NodeInfoAndSigned

View File

@ -28,7 +28,6 @@ import net.corda.core.utilities.seconds
import net.corda.node.VersionInfo
import net.corda.node.internal.AbstractNode
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.internal.NetworkParametersStorage
import net.corda.node.internal.NodeFlowManager
import net.corda.node.services.api.FlowStarter
import net.corda.node.services.api.ServiceHubInternal

View File

@ -2,11 +2,12 @@ package net.corda.testing.node.internal
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.NetworkParametersStorage
import net.corda.core.internal.SignedDataWithCert
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.serialization.serialize
import net.corda.node.internal.NetworkParametersStorage
import net.corda.nodeapi.internal.network.SignedNetworkParameters
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.internal.withTestSerializationEnvIfNotSet
@ -15,6 +16,7 @@ import java.time.Instant
class MockNetworkParametersStorage(private var currentParameters: NetworkParameters = testNetworkParameters(modifiedTime = Instant.MIN)) : NetworkParametersStorage {
private val hashToParametersMap: HashMap<SecureHash, NetworkParameters> = HashMap()
private val hashToSignedParametersMap: HashMap<SecureHash, SignedNetworkParameters> = HashMap()
init {
storeCurrentParameters()
@ -29,6 +31,12 @@ class MockNetworkParametersStorage(private var currentParameters: NetworkParamet
setCurrentParametersUnverified(currentSignedParameters.verifiedNetworkMapCert(trustRoot))
}
override fun lookupSigned(hash: SecureHash): SignedDataWithCert<NetworkParameters>? {
return hashToSignedParametersMap[hash]
}
override fun hasParameters(hash: SecureHash): Boolean = hash in hashToParametersMap
override val currentHash: SecureHash
get() {
return withTestSerializationEnvIfNotSet {
@ -42,6 +50,7 @@ class MockNetworkParametersStorage(private var currentParameters: NetworkParamet
val networkParameters = signedNetworkParameters.verified()
val hash = signedNetworkParameters.raw.hash
hashToParametersMap[hash] = networkParameters
hashToSignedParametersMap[hash] = signedNetworkParameters
}
override fun getHistoricNotary(party: Party): NotaryInfo? {