Merge pull request #7783 from corda/merge-release/os/4.10-release/os/4.11-2024-08-12-297

ENT-12072: Merging forward updates from release/os/4.10 to release/os/4.11 - 2024-08-12
This commit is contained in:
Rick Parker 2024-08-13 10:26:16 +01:00 committed by GitHub
commit ccfb8a932d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 157 additions and 4 deletions

View File

@ -75,8 +75,8 @@ open class ReceiveTransactionFlow constructor(private val otherSideSession: Flow
val stx = resolvePayload(payload) val stx = resolvePayload(payload)
stx.pushToLoggingContext() stx.pushToLoggingContext()
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.") logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.")
checkParameterHash(stx.networkParametersHash)
subFlow(ResolveTransactionsFlow(stx, otherSideSession, statesToRecord, deferredAck)) subFlow(ResolveTransactionsFlow(stx, otherSideSession, statesToRecord, deferredAck))
checkParameterHash(stx.networkParametersHash)
logger.info("Transaction dependencies resolution completed.") logger.info("Transaction dependencies resolution completed.")
verifyTx(stx, checkSufficientSignatures) verifyTx(stx, checkSufficientSignatures)
if (checkSufficientSignatures) { if (checkSufficientSignatures) {

View File

@ -1,14 +1,33 @@
package net.corda.node.services.identity package net.corda.node.services.identity
import co.paralleluniverse.fibers.Suspendable
import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
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.internal.createDirectories import net.corda.core.internal.createDirectories
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.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS import net.corda.finance.DOLLARS
import net.corda.finance.USD import net.corda.finance.USD
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.flows.CashPaymentFlow
import net.corda.finance.schemas.CashSchemaV1
import net.corda.finance.workflows.getCashBalance import net.corda.finance.workflows.getCashBalance
import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.NotaryConfig
import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.DevIdentityGenerator
@ -25,12 +44,16 @@ import net.corda.testing.node.internal.FINANCE_CORDAPPS
import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.TestStartedNode import net.corda.testing.node.internal.TestStartedNode
import net.corda.testing.node.internal.enclosedCordapp
import net.corda.testing.node.internal.startFlow import net.corda.testing.node.internal.startFlow
import org.junit.After import org.junit.After
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@RunWith(Parameterized::class) @RunWith(Parameterized::class)
class NotaryCertificateRotationTest(private val validating: Boolean) { class NotaryCertificateRotationTest(private val validating: Boolean) {
@ -91,8 +114,9 @@ class NotaryCertificateRotationTest(private val validating: Boolean) {
val bob2 = mockNet.restartNode(bob) val bob2 = mockNet.restartNode(bob)
val charlie = mockNet.createPartyNode(CHARLIE_NAME) val charlie = mockNet.createPartyNode(CHARLIE_NAME)
// Save previous network parameters for subsequent backchain verification. // Save previous network parameters for subsequent backchain verification, because not persistent in mock network
mockNet.nodes.forEach { it.services.networkParametersService.saveParameters(ca.sign(mockNet.networkParameters)) } 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. // Verify that notary identity has been changed.
assertEquals(listOf(newNotaryIdentity), alice2.services.networkMapCache.notaryIdentities) 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(0.DOLLARS, bob2.services.getCashBalance(USD))
assertEquals(7300.DOLLARS, charlie.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<Cash.State> = 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<Cash.State> = 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<Unit>() {
@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<Unit>() {
@Suspendable
override fun call() {
subFlow(ReceiveTransactionFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE))
}
}
} }

View File

@ -400,7 +400,12 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
if (candidate != null && candidate != party) { if (candidate != null && candidate != party) {
// Party doesn't match existing well-known party: check that the key is registered, otherwise return null. // 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" } 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 { } else {
// Party is a well-known party or well-known party doesn't exist: skip checks. // 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 // If the notary is not in the network map cache, try getting it from the network parameters

View File

@ -506,6 +506,18 @@ open class InternalMockNetwork(cordappPackages: List<String> = emptyList(),
return node return node
} }
fun hideNode(
node: TestStartedNode
) {
_nodes.remove(node.internals)
}
fun unhideNode(
node: TestStartedNode
) {
_nodes.add(node.internals)
}
fun restartNode( fun restartNode(
node: TestStartedNode, node: TestStartedNode,
parameters: InternalMockNodeParameters = InternalMockNodeParameters(), parameters: InternalMockNodeParameters = InternalMockNodeParameters(),