diff --git a/.ci/dev/publish-branch/Jenkinsfile.nightly b/.ci/dev/publish-branch/Jenkinsfile.nightly index cfb17f0209..b560329b81 100644 --- a/.ci/dev/publish-branch/Jenkinsfile.nightly +++ b/.ci/dev/publish-branch/Jenkinsfile.nightly @@ -34,6 +34,10 @@ pipeline { // in the name ARTIFACTORY_BUILD_NAME = "Corda / Publish / Publish Nightly to Artifactory" .replaceAll("/", " :: ") + BUILD_CACHE_CREDENTIALS = credentials('gradle-ent-cache-credentials') + BUILD_CACHE_PASSWORD = "${env.BUILD_CACHE_CREDENTIALS_PSW}" + BUILD_CACHE_USERNAME = "${env.BUILD_CACHE_CREDENTIALS_USR}" + USE_CACHE = 'corda-remotes' DOCKER_URL = "https://index.docker.io/v1/" JAVA_HOME = "/usr/lib/jvm/java-17-amazon-corretto" } diff --git a/build.gradle b/build.gradle index 656c963722..2c02bfeedd 100644 --- a/build.gradle +++ b/build.gradle @@ -165,7 +165,9 @@ buildscript { } } mavenCentral() - jcenter() + maven { + url "${publicArtifactURL}/jcenter-backup" + } } } dependencies { @@ -406,7 +408,9 @@ allprojects { } } mavenCentral() - jcenter() + maven { + url "${publicArtifactURL}/jcenter-backup" + } } } diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 31bbc4cf3b..3fcd20ca47 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -75,8 +75,8 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow val stx = resolvePayload(payload) stx.pushToLoggingContext() logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") - checkParameterHash(stx.networkParametersHash) subFlow(ResolveTransactionsFlow(stx, otherSideSession, statesToRecord, deferredAck)) + checkParameterHash(stx.networkParametersHash) logger.info("Transaction dependencies resolution completed.") verifyTx(stx, checkSufficientSignatures) if (checkSufficientSignatures) { 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 index 150af49ff1..0506cc0c7f 100644 --- 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 @@ -1,13 +1,30 @@ package net.corda.node.services.identity -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.whenever +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveTransactionFlow +import net.corda.core.flows.SendTransactionFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.Vault +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.builder +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.workflows.getCashBalance import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.internal.DevIdentityGenerator @@ -24,13 +41,19 @@ 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.enclosedCordapp 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 org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import java.util.Currency import kotlin.io.path.createDirectories import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull @RunWith(Parameterized::class) class NotaryCertificateRotationTest(private val validating: Boolean) { @@ -91,8 +114,9 @@ class NotaryCertificateRotationTest(private val validating: Boolean) { 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)) } + // Save previous network parameters for subsequent backchain verification, because not persistent in mock network + alice2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) + bob2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) // Verify that notary identity has been changed. assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities) @@ -126,4 +150,116 @@ class NotaryCertificateRotationTest(private val validating: Boolean) { assertEquals(0.DOLLARS, bob2.services.getCashBalance(USD)) assertEquals(7300.DOLLARS, charlie.services.getCashBalance(USD)) } + + @Test(timeout = 300_000) + fun `rotate notary identity and new node receives netparams and understands old notary`() { + mockNet = InternalMockNetwork( + cordappsForAllNodes = FINANCE_CORDAPPS + enclosedCordapp(), + notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, validating)), + initialNetworkParameters = testNetworkParameters() + ) + 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() + + val oldHash = alice.services.networkParametersService.currentHash + + // 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. + mockNet.createNode(InternalMockNodeParameters( + legalName = DUMMY_NOTARY_NAME, + configOverrides = { doReturn(NotaryConfig(validating)).whenever(it).notary } + )) + val alice2 = mockNet.restartNode(alice) + val bob2 = mockNet.restartNode(bob) + // We hide the old notary as trying to simulate it's replacement + mockNet.hideNode(mockNet.defaultNotaryNode) + val charlie = mockNet.createPartyNode(CHARLIE_NAME) + + // Save previous network parameters for subsequent backchain verification, because not persistent in mock network + alice2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) + bob2.internals.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) + + assertNotNull(alice2.services.networkParametersService.lookup(oldHash)) + assertNotNull(bob2.services.networkParametersService.lookup(oldHash)) + assertNull(charlie.services.networkParametersService.lookup(oldHash)) + + // 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)) + + // Now send an existing transaction on Bob (from before rotation) to Charlie + val bobVault: Vault.Page = bob2.services.vaultService.queryBy(generateCashCriteria(USD)) + assertEquals(1, bobVault.states.size) + val handle = bob2.services.startFlow(RpcSendTransactionFlow(bobVault.states[0].ref.txhash, charlie.party)) + mockNet.runNetwork() + // Check flow completed successfully + assertEquals(handle.resultFuture.getOrThrow(), Unit) + + // Check Charlie recorded it in the vault (could resolve notary, for example) + val charlieVault: Vault.Page = charlie.services.vaultService.queryBy(generateCashCriteria(USD)) + assertEquals(1, charlieVault.states.size) + + // Check Charlie gained the network parameters from before the rotation + assertNotNull(charlie.services.networkParametersService.lookup(oldHash)) + + // We unhide the old notary so it can be shutdown + mockNet.unhideNode(mockNet.defaultNotaryNode) + } + + private fun generateCashCriteria(currency: Currency): QueryCriteria { + val stateCriteria = QueryCriteria.FungibleAssetQueryCriteria() + val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(currency.currencyCode) } + // This query should only return cash states the calling node is a participant of (meaning they can be modified/spent). + val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, relevancyStatus = Vault.RelevancyStatus.ALL) + return stateCriteria.and(ccyCriteria) + } + + @StartableByRPC + @InitiatingFlow + class RpcSendTransactionFlow(private val tx: SecureHash, private val party: Party) : FlowLogic() { + @Suspendable + override fun call() { + val session = initiateFlow(party) + val stx: SignedTransaction = serviceHub.validatedTransactions.getTransaction(tx)!! + subFlow(SendTransactionFlow(session, stx)) + } + } + + @InitiatedBy(RpcSendTransactionFlow::class) + class RpcSendTransactionResponderFlow(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + subFlow(ReceiveTransactionFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE)) + } + } } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index a6b7fac01c..9d0ffb1fb2 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -399,7 +399,12 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri if (candidate != null && candidate != party) { // Party doesn't match existing well-known party: check that the key is registered, otherwise return null. require(party.name == candidate.name) { "Candidate party $candidate does not match expected $party" } - keyToParty[party.owningKey.toStringShort()]?.let { candidate } + // If the party is a whitelisted notary, then it was just a rotated notary key + if (party in notaryIdentityCache) { + candidate + } else { + keyToParty[party.owningKey.toStringShort()]?.let { candidate } + } } else { // Party is a well-known party or well-known party doesn't exist: skip checks. // If the notary is not in the network map cache, try getting it from the network parameters diff --git a/release-tools/testing/requirements.txt b/release-tools/testing/requirements.txt index b4bc32760d..ebec25c140 100644 --- a/release-tools/testing/requirements.txt +++ b/release-tools/testing/requirements.txt @@ -1,3 +1,6 @@ jira==2.0.0 keyring==13.1.0 termcolor==1.1.0 +urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability +requests>=2.32.0 # not directly required, pinned by Snyk to avoid a vulnerability +setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 3ff7b5c363..aaadddf5b3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -499,6 +499,18 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), return node } + fun hideNode( + node: TestStartedNode + ) { + _nodes.remove(node.internals) + } + + fun unhideNode( + node: TestStartedNode + ) { + _nodes.add(node.internals) + } + fun restartNode( node: TestStartedNode, parameters: InternalMockNodeParameters = InternalMockNodeParameters(),