mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merge pull request #7821 from corda/adel/ENT-12072-and-ENT-12073
ENT-12072 ENT-12073: Check notary whitelist when resolving old identities and don't depend on network map availability first for old network parameters
This commit is contained in:
commit
76ff377dca
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user