From 551b3f0811309e12261ce1c34f862f0a46e47dc0 Mon Sep 17 00:00:00 2001 From: Denis Rekalov Date: Fri, 16 Oct 2020 15:53:04 +0300 Subject: [PATCH] CORDA-4054: combine different identities of the same notary after its key rotation (#6734) --- .../client/jackson/JacksonSupportTest.kt | 2 + .../flows/CollectSignaturesFlowTests.kt | 4 +- .../TransactionSerializationTests.kt | 2 +- .../transactions/TransactionBuilderTest.kt | 2 + .../kotlin/net/corda/core/flows/NotaryFlow.kt | 14 +- .../core/internal/notary/NotaryServiceFlow.kt | 4 +- .../corda/core/internal/notary/NotaryUtils.kt | 4 +- .../core/node/services/NetworkMapCache.kt | 2 +- .../core/transactions/BaseTransactions.kt | 17 ++- .../transactions/NotaryChangeTransactions.kt | 2 +- .../core/transactions/TransactionBuilder.kt | 27 +++- ...tachmentsClassLoaderStaticContractTests.kt | 2 + .../identity/NotaryCertificateRotationTest.kt | 129 ++++++++++++++++++ .../network/PersistentNetworkMapCache.kt | 11 +- .../transactions/NonValidatingNotaryFlow.kt | 3 +- .../corda/irs/api/NodeInterestRatesTest.kt | 4 +- .../flow/TransactionGraphSearchTests.kt | 6 +- 17 files changed, 207 insertions(+), 28 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index b6f8d20f23..5edd886de5 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -26,6 +26,7 @@ import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkParametersService import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.CordaSerializable @@ -242,6 +243,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: val attachmentStorage = rigorousMock() doReturn(attachmentStorage).whenever(services).attachments doReturn(mock()).whenever(services).validatedTransactions + doReturn(mock()).whenever(services).identityService val attachment = rigorousMock() doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId) doReturn(attachmentId).whenever(attachment).id diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/CollectSignaturesFlowTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/CollectSignaturesFlowTests.kt index f0d13e2123..f0287086c6 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/CollectSignaturesFlowTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/CollectSignaturesFlowTests.kt @@ -20,7 +20,6 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.excludeHostNode import net.corda.core.identity.groupAbstractPartyByWellKnownParty -import net.corda.core.node.services.IdentityService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow @@ -32,7 +31,6 @@ import net.corda.testing.core.TestIdentity import net.corda.testing.core.singleIdentity import net.corda.coretesting.internal.matchers.flow.willReturn import net.corda.coretesting.internal.matchers.flow.willThrow -import net.corda.coretesting.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP import net.corda.testing.node.internal.InternalMockNetwork @@ -47,7 +45,7 @@ import java.security.PublicKey class CollectSignaturesFlowTests : WithContracts { companion object { private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) - private val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), miniCorp, rigorousMock()) + private val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), miniCorp) private val classMockNet = InternalMockNetwork(cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, enclosedCordapp())) private const val MAGIC_NUMBER = 1337 diff --git a/core-tests/src/test/kotlin/net/corda/coretests/serialization/TransactionSerializationTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/serialization/TransactionSerializationTests.kt index bce4e67224..2a37727aa4 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/serialization/TransactionSerializationTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/serialization/TransactionSerializationTests.kt @@ -80,7 +80,7 @@ class TransactionSerializationTests { //override mock implementation with a real one override fun loadContractAttachment(stateRef: StateRef): Attachment = servicesForResolution.loadContractAttachment(stateRef) } - val notaryServices = MockServices(listOf("net.corda.coretests.serialization"), DUMMY_NOTARY.name, rigorousMock(), DUMMY_NOTARY_KEY) + val notaryServices = MockServices(listOf("net.corda.coretests.serialization"), DUMMY_NOTARY.name, key = DUMMY_NOTARY_KEY) lateinit var tx: TransactionBuilder @Before diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt index 083d91753f..1f3442e2a8 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt @@ -13,6 +13,7 @@ import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.node.ServicesForResolution import net.corda.core.node.ZoneVersionTooLowException import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkParametersService import net.corda.core.serialization.serialize import net.corda.core.transactions.TransactionBuilder @@ -51,6 +52,7 @@ class TransactionBuilderTest { doReturn(cordappProvider).whenever(services).cordappProvider doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) doReturn(networkParameters).whenever(services).networkParameters + doReturn(mock()).whenever(services).identityService val attachmentStorage = rigorousMock() doReturn(attachmentStorage).whenever(services).attachments diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index d24d33701d..93e866c62f 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -82,9 +82,13 @@ class NotaryFlow { */ protected fun checkTransaction(): Party { val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary") - check(serviceHub.networkMapCache.isNotary(notaryParty)) { "$notaryParty is not a notary on the network" } - check(serviceHub.loadStates(stx.inputs.toSet() + stx.references.toSet()).all { it.state.notary == notaryParty }) { - "Input states and reference input states must have the same Notary" + val notaries = setOf(notaryParty) + serviceHub.loadStates(stx.inputs.toSet() + stx.references.toSet()).map { it.state.notary } + notaries.forEach { + check(serviceHub.networkMapCache.isNotary(it)) { "${it.description()} is not a notary on the network" } + // Transaction can combine different identities of the same notary after key rotation. + check(it.name == notaryParty.name) { + "Input states and reference input states must have the same Notary as the transaction Notary" + } } if (!skipVerification) { @@ -117,10 +121,10 @@ class NotaryFlow { and can't be used for regular transactions. */ 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" + "Notary ${notaryParty.description()} is not on the network parameter whitelist. A non-whitelisted notary can only be used for notary change transactions" } 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.") + ?: throw IllegalStateException("The notary party ${notaryParty.description()} specified by transaction ${stx.id}, is not recognised as a current or historic notary.") historicNotary.validating } else serviceHub.networkMapCache.isValidatingNotary(notaryParty) } diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt index e105d945a7..307b675046 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.toStringShort import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.internal.IdempotentFlow @@ -108,7 +109,8 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: @Suspendable private fun checkNotary(notary: Party?) { require(notary?.owningKey == service.notaryIdentityKey) { - "The notary specified on the transaction: [$notary] does not match the notary service's identity: [${service.notaryIdentityKey}] " + "The notary specified on the transaction: [${notary?.description()}] does not match the notary service's identity:" + + " [${service.notaryIdentityKey.toStringShort()}] " } } diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt index 29de907927..77e8096241 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt @@ -40,7 +40,9 @@ fun NotarisationRequest.verifySignature(requestSignature: NotarisationRequestSig */ fun NotarisationResponse.validateSignatures(txId: SecureHash, notary: Party) { val signingKeys = signatures.map { it.by } - require(notary.owningKey.isFulfilledBy(signingKeys)) { "Insufficient signatures to fulfill the notary signing requirement for $notary" } + require(notary.owningKey.isFulfilledBy(signingKeys)) { + "Insufficient signatures to fulfill the notary signing requirement for ${notary.description()}" + } signatures.forEach { it.verify(txId) } } diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 0ab6627f0e..8e46572f1c 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -120,7 +120,7 @@ interface NetworkMapCacheBase { // DOCEND 2 /** Returns true if and only if the given [Party] is a notary, which is defined by the network parameters. */ - fun isNotary(party: Party): Boolean = party in notaryIdentities + fun isNotary(party: Party): Boolean /** * Returns true if and only if the given [Party] is validating notary. For every party that is a validating notary, diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt index 6b08df0368..363c9b5da3 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt @@ -42,9 +42,12 @@ abstract class FullTransaction : BaseTransaction() { private fun checkInputsAndReferencesHaveSameNotary() { if (inputs.isEmpty() && references.isEmpty()) return - val notaries = (inputs + references).map { it.state.notary }.toHashSet() + // Transaction can combine different identities of the same notary after key rotation. + val notaries = (inputs + references).map { it.state.notary.name }.toHashSet() check(notaries.size == 1) { "All inputs and reference inputs must point to the same notary" } - check(notaries.single() == notary) { "The specified notary must be the one specified by all inputs and input references" } + check(notaries.single() == notary?.name) { + "The specified transaction notary must be the one specified by all inputs and input references" + } } /** Make sure the assigned notary is part of the network parameter whitelist. */ @@ -53,8 +56,14 @@ abstract class FullTransaction : BaseTransaction() { // Network parameters will never be null if the transaction is resolved from a CoreTransaction rather than constructed directly. networkParameters?.let { parameters -> val notaryWhitelist = parameters.notaries.map { it.identity } - check(notaryParty in notaryWhitelist) { - "Notary ($notaryParty) specified by the transaction is not on the network parameter whitelist: [${notaryWhitelist.joinToString()}]" + // Transaction can combine different identities of the same notary after key rotation. + // Each of these identities should be whitelisted. + val notaries = setOf(notaryParty) + (inputs + references).map { it.state.notary } + notaries.forEach { + check(it in notaryWhitelist) { + "Notary [${it.description()}] specified by the transaction is not on the network parameter whitelist: " + + " [${notaryWhitelist.joinToString { party -> party.description() }}]" + } } } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index efa3aa57a7..de3a8d30f9 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -156,7 +156,7 @@ private constructor( networkParameters?.let { parameters -> val notaryWhitelist = parameters.notaries.map { it.identity } check(newNotary in notaryWhitelist) { - "The output notary $newNotary is not whitelisted in the attached network parameters." + "The output notary ${newNotary.description()} is not whitelisted in the attached network parameters." } } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 3365ac7783..23d829030d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -154,6 +154,7 @@ open class TransactionBuilder( if (referenceStates.isNotEmpty()) { services.ensureMinimumPlatformVersion(4, "Reference states") } + resolveNotary(services) val (allContractAttachments: Collection, resolvedOutputs: List>) = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext) @@ -635,8 +636,10 @@ open class TransactionBuilder( private fun checkNotary(stateAndRef: StateAndRef<*>) { val notary = stateAndRef.state.notary - require(notary == this.notary) { - "Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"." + // Transaction can combine different identities of the same notary after key rotation. + require(notary.name == this.notary?.name) { + "Input state requires notary \"${notary.description()}\" which does not match" + + " the transaction notary \"${this.notary?.description()}\"." } } @@ -648,7 +651,25 @@ open class TransactionBuilder( } } - private fun checkReferencesUseSameNotary() = referencesWithTransactionState.map { it.notary }.toSet().size == 1 + // Transaction can combine different identities of the same notary after key rotation. + private fun checkReferencesUseSameNotary() = referencesWithTransactionState.map { it.notary.name }.toSet().size == 1 + + // Automatically correct notary after its key rotation + private fun resolveNotary(services: ServicesForResolution) { + notary?.let { + val activeNotary = services.identityService.wellKnownPartyFromX500Name(it.name) + if (activeNotary != null && activeNotary != it) { + log.warn("Replacing notary on the transaction '${it.description()}' with '${activeNotary.description()}'.") + notary = activeNotary + } + outputs.forEachIndexed { index, transactionState -> + if (activeNotary != null && activeNotary != transactionState.notary) { + log.warn("Replacing notary on the transaction output '${it.description()}' with '${activeNotary.description()}'.") + outputs[index] = transactionState.copy(notary = activeNotary) + } + } + } + } /** * If any inputs or outputs added to the [TransactionBuilder] contain [StatePointer]s, then this method is used diff --git a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt index 500e0e2873..e6aa5e3963 100644 --- a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -11,6 +11,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkParametersService import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize @@ -91,6 +92,7 @@ class AttachmentsClassLoaderStaticContractTests { val contractAttachmentId = SecureHash.randomSHA256() doReturn(listOf(contractAttachmentId)).whenever(attachmentStorage) .getLatestContractAttachments(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID) + doReturn(mock()).whenever(it).identityService } @Test(timeout=300_000) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt new file mode 100644 index 0000000000..452fb96cb0 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/identity/NotaryCertificateRotationTest.kt @@ -0,0 +1,129 @@ +package net.corda.node.services.identity + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.internal.createDirectories +import net.corda.core.utilities.OpaqueBytes +import net.corda.finance.DOLLARS +import net.corda.finance.USD +import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.workflows.getCashBalance +import net.corda.node.services.config.NotaryConfig +import net.corda.nodeapi.internal.DevIdentityGenerator +import net.corda.nodeapi.internal.createDevNetworkMapCa +import net.corda.nodeapi.internal.network.NetworkParametersCopier +import net.corda.testing.common.internal.addNotary +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.node.MockNetworkNotarySpec +import net.corda.testing.node.internal.FINANCE_CORDAPPS +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNodeParameters +import net.corda.testing.node.internal.TestStartedNode +import net.corda.testing.node.internal.startFlow +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import kotlin.test.assertEquals + +@RunWith(Parameterized::class) +class NotaryCertificateRotationTest(private val validating: Boolean) { + private val ref = OpaqueBytes.of(0x01) + + private val TestStartedNode.party get() = info.legalIdentities.first() + + private lateinit var mockNet: InternalMockNetwork + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "validating = {0}") + fun data() = listOf(false, true) + } + + @After + fun tearDown() { + mockNet.stopNodes() + } + + @Test(timeout = 300_000) + fun `rotate notary identity`() { + mockNet = InternalMockNetwork( + cordappsForAllNodes = FINANCE_CORDAPPS, + notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, validating)) + ) + val alice = mockNet.createPartyNode(ALICE_NAME) + val bob = mockNet.createPartyNode(BOB_NAME) + + // Issue states and notarize them with initial notary identity. + alice.services.startFlow(CashIssueFlow(1000.DOLLARS, ref, mockNet.defaultNotaryIdentity)) + alice.services.startFlow(CashIssueAndPaymentFlow(2000.DOLLARS, ref, alice.party, false, mockNet.defaultNotaryIdentity)) + alice.services.startFlow(CashIssueAndPaymentFlow(4000.DOLLARS, ref, bob.party, false, mockNet.defaultNotaryIdentity)) + mockNet.runNetwork() + + // Rotate notary identity and update network parameters. + val newNotaryIdentity = DevIdentityGenerator.installKeyStoreWithNodeIdentity( + mockNet.baseDirectory(mockNet.nextNodeId), + DUMMY_NOTARY_NAME + ) + val newNetworkParameters = testNetworkParameters(epoch = 2) + .addNotary(mockNet.defaultNotaryIdentity, validating) + .addNotary(newNotaryIdentity, validating) + val ca = createDevNetworkMapCa() + NetworkParametersCopier(newNetworkParameters, ca, overwriteFile = true).apply { + install(mockNet.baseDirectory(alice)) + install(mockNet.baseDirectory(bob)) + install(mockNet.baseDirectory(mockNet.nextNodeId)) + install(mockNet.baseDirectory(mockNet.nextNodeId + 1).apply { createDirectories() }) + } + + // Start notary with new identity and restart nodes. + val notary2 = mockNet.createNode(InternalMockNodeParameters( + legalName = DUMMY_NOTARY_NAME, + configOverrides = { doReturn(NotaryConfig(validating)).whenever(it).notary } + )) + val alice2 = mockNet.restartNode(alice) + val bob2 = mockNet.restartNode(bob) + val charlie = mockNet.createPartyNode(CHARLIE_NAME) + + // Save previous network parameters for subsequent backchain verification. + mockNet.nodes.forEach { it.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) } + + // Verify that notary identity has been changed. + assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities) + assertEquals(listOf(newNotaryIdentity), bob2.services.networkMapCache.notaryIdentities) + assertEquals(listOf(newNotaryIdentity), charlie.services.networkMapCache.notaryIdentities) + + assertEquals(newNotaryIdentity, alice2.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME)) + assertEquals(newNotaryIdentity, bob2.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME)) + assertEquals(newNotaryIdentity, charlie.services.identityService.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME)) + + assertEquals(newNotaryIdentity, alice2.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity)) + assertEquals(newNotaryIdentity, bob2.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity)) + assertEquals(newNotaryIdentity, charlie.services.identityService.wellKnownPartyFromAnonymous(mockNet.defaultNotaryIdentity)) + + // Move states notarized with previous notary identity. + alice2.services.startFlow(CashPaymentFlow(3000.DOLLARS, bob2.party, false)) + mockNet.runNetwork() + bob2.services.startFlow(CashPaymentFlow(7000.DOLLARS, charlie.party, false)) + mockNet.runNetwork() + charlie.services.startFlow(CashPaymentFlow(7000.DOLLARS, alice2.party, false)) + mockNet.runNetwork() + + // Combine states notarized with previous and present notary identities. + bob2.services.startFlow(CashIssueAndPaymentFlow(300.DOLLARS, ref, alice2.party, false, notary2.party)) + mockNet.runNetwork() + alice2.services.startFlow(CashPaymentFlow(7300.DOLLARS, charlie.party, false)) + mockNet.runNetwork() + + // Verify that the ledger contains expected states. + assertEquals(0.DOLLARS, alice2.services.getCashBalance(USD)) + assertEquals(0.DOLLARS, bob2.services.getCashBalance(USD)) + assertEquals(7300.DOLLARS, charlie.services.getCashBalance(USD)) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index c7400b3b3c..48c985bbe6 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -58,7 +58,13 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, @Volatile private lateinit var notaries: List + @Volatile + private lateinit var rotatedNotaries: Set + + // Notary whitelist may contain multiple identities with the same X.500 name after certificate rotation. + // Exclude duplicated entries, which are not present in the network map. override val notaryIdentities: List get() = notaries.map { it.identity } + .filterNot { it.name in rotatedNotaries && it != getPeerCertificateByLegalName(it.name)?.party } override val allNodeHashes: List get() { @@ -74,7 +80,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, } fun start(notaries: List) { - this.notaries = notaries + onNewNotaryList(notaries) } override fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo? { @@ -98,6 +104,8 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, } } + override fun isNotary(party: Party): Boolean = notaries.any { it.identity == party } + override fun isValidatingNotary(party: Party): Boolean = notaries.any { it.validating && it.identity == party } override fun getPartyInfo(party: Party): PartyInfo? { @@ -426,5 +434,6 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, override fun onNewNotaryList(notaries: List) { this.notaries = notaries + this.rotatedNotaries = notaries.groupBy { it.identity.name }.filter { it.value.size > 1 }.keys } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index 3c2b2bc739..7ae66c2c21 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -91,7 +91,8 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: SinglePart val notaryWhitelist = networkParameters.notaries.map { it.identity } check(notary in notaryWhitelist) { - "Notary specified by the transaction ($notary) is not on the network parameter whitelist: ${notaryWhitelist.joinToString()}" + "Notary [${notary.description()}] specified by the transaction is not on the network parameter whitelist: " + + " [${notaryWhitelist.joinToString { party -> party.description() }}]" } } diff --git a/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 1b9c75d25b..8206bdc2f0 100644 --- a/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -14,10 +14,10 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* import net.corda.testing.internal.configureDatabase -import net.corda.coretesting.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.createMockCordaService +import net.corda.testing.node.makeTestIdentityService import org.junit.After import org.junit.Assert.* import org.junit.Before @@ -50,7 +50,7 @@ class NodeInterestRatesTest { EURIBOR 2016-03-15 2M = 0.111 """.trimIndent()) private val dummyCashIssuer = TestIdentity(CordaX500Name("Cash issuer", "London", "GB")) - private val services = MockServices(listOf("net.corda.finance.contracts.asset"), dummyCashIssuer, rigorousMock(), MEGA_CORP_KEY) + private val services = MockServices(listOf("net.corda.finance.contracts.asset"), dummyCashIssuer, makeTestIdentityService(), MEGA_CORP_KEY) // This is safe because MockServices only ever have a single identity private val identity = services.myInfo.singleIdentity() diff --git a/samples/trader-demo/workflows-trader/src/test/kotlin/net/corda/traderdemo/flow/TransactionGraphSearchTests.kt b/samples/trader-demo/workflows-trader/src/test/kotlin/net/corda/traderdemo/flow/TransactionGraphSearchTests.kt index e59a56eb59..941c9df731 100644 --- a/samples/trader-demo/workflows-trader/src/test/kotlin/net/corda/traderdemo/flow/TransactionGraphSearchTests.kt +++ b/samples/trader-demo/workflows-trader/src/test/kotlin/net/corda/traderdemo/flow/TransactionGraphSearchTests.kt @@ -3,7 +3,6 @@ package net.corda.traderdemo.flow import net.corda.core.contracts.CommandData import net.corda.core.crypto.newSecureRandom import net.corda.core.identity.CordaX500Name -import net.corda.core.node.services.IdentityService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -13,7 +12,6 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.core.dummyCommand -import net.corda.coretesting.internal.rigorousMock import net.corda.testing.node.MockServices import net.corda.testing.node.internal.MockTransactionStorage import org.junit.Rule @@ -47,8 +45,8 @@ class TransactionGraphSearchTests { * @param signer signer for the two transactions and their commands. */ fun buildTransactions(command: CommandData): GraphTransactionStorage { - val megaCorpServices = MockServices(listOf("net.corda.testing.contracts"), megaCorp, rigorousMock()) - val notaryServices = MockServices(listOf("net.corda.testing.contracts"), dummyNotary, rigorousMock()) + val megaCorpServices = MockServices(listOf("net.corda.testing.contracts"), megaCorp) + val notaryServices = MockServices(listOf("net.corda.testing.contracts"), dummyNotary) val originBuilder = TransactionBuilder(dummyNotary.party) .addOutputState(DummyState(random31BitValue()), DummyContract.PROGRAM_ID) .addCommand(command, megaCorp.publicKey)