ENT-12072 ENT-12073: Check notary whitelist when resolving old identities and don't depend on network map availability first for old network parameters (#7781)

Nodes currently will try and resolve network parameters from the network map and fail if it not available, rather than preferring the availability of a node they are currently interacting with.

A migrated notary identity could not be resolved on new nodes added post-migration, but the old identity is available in the network parameter notary whitelist.

Added a test that covers both bugs in a single reproduction test that simulates the scenario in which both were uncovered.
This commit is contained in:
Rick Parker 2024-08-12 19:19:30 +01:00 committed by Adel El-Beik
parent 64f900dc38
commit fd04fddd70
4 changed files with 162 additions and 5 deletions

View File

@ -1,7 +1,11 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.contracts.AttachmentResolutionException
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.internal.ResolveTransactionsFlow
import net.corda.core.internal.checkParameterHash
import net.corda.core.internal.pushToLoggingContext
@ -46,8 +50,8 @@ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSid
val stx = otherSideSession.receive<SignedTransaction>().unwrap {
it.pushToLoggingContext()
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.")
checkParameterHash(it.networkParametersHash)
subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord))
checkParameterHash(it.networkParametersHash)
logger.info("Transaction dependencies resolution completed.")
try {
it.verify(serviceHub, checkSufficientSignatures)

View File

@ -1,14 +1,33 @@
package net.corda.node.services.identity
import co.paralleluniverse.fibers.Suspendable
import com.nhaarman.mockito_kotlin.doReturn
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.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
@ -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.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 java.util.*
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<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

@ -323,7 +323,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

View File

@ -503,6 +503,18 @@ open class InternalMockNetwork(cordappPackages: List<String> = 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(),