From 65c5ce65a647a99a044acf02b1dbc6d0c37d9d08 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 24 Aug 2017 11:19:34 +0100 Subject: [PATCH] Adapt CollectSignaturesFlow to handle anonymous transactions Adapt CollectSignaturesFlow to handle anonymous transactions where the keys signing commands on a transaction are not necessarily the well known identity of the participating nodes. Also prepares for any potential move away from nodes having a single primary identity by requiring flows to specify the identities they're using for a transaction. --- .../corda/core/flows/CollectSignaturesFlow.kt | 65 ++++++++++++------- .../core/flows/CollectSignaturesFlowTests.kt | 23 ++++--- .../node/messaging/TwoPartyTradeFlowTests.kt | 5 ++ .../corda/netmap/simulation/IRSSimulation.kt | 5 ++ 4 files changed, 66 insertions(+), 32 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt index 390f5aaec1..9f95d897a5 100644 --- a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.toBase58String +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction @@ -56,12 +57,14 @@ import java.security.PublicKey * val stx = subFlow(CollectSignaturesFlow(ptx)) * * @param partiallySignedTx Transaction to collect the remaining signatures for + * @param myOptionalKeys set of keys in the transaction which are owned by this node. This includes keys used on commands, not + * just in the states. If null, the default well known identity of the node is used. */ // TODO: AbstractStateReplacementFlow needs updating to use this flow. -// TODO: Update this flow to handle randomly generated keys when that work is complete. -class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, - override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic() { - +class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction, + val myOptionalKeys: Iterable?, + override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic() { + @JvmOverloads constructor(partiallySignedTx: SignedTransaction, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, null, progressTracker) companion object { object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.") object VERIFYING : ProgressTracker.Step("Verifying collected signatures.") @@ -72,16 +75,14 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, } @Suspendable override fun call(): SignedTransaction { - // TODO: Revisit when key management is properly fleshed out. - // This will break if a party uses anything other than their legalIdentityKey. // Check the signatures which have already been provided and that the transaction is valid. // Usually just the Initiator and possibly an oracle would have signed at this point. - val myKey = serviceHub.myInfo.legalIdentity.owningKey + val myKeys: Iterable = myOptionalKeys ?: listOf(serviceHub.myInfo.legalIdentity.owningKey) val signed = partiallySignedTx.sigs.map { it.by } val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed // One of the signatures collected so far MUST be from the initiator of this flow. - require(partiallySignedTx.sigs.any { it.by == myKey }) { + require(partiallySignedTx.sigs.any { it.by in myKeys }) { "The Initiator of CollectSignaturesFlow must have signed the transaction." } @@ -100,7 +101,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, if (unsigned.isEmpty()) return partiallySignedTx // Collect signatures from all counter-parties and append them to the partially signed transaction. - val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it) } + val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it.first, it.second) } val stx = partiallySignedTx + counterpartySignatures // Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures. @@ -112,23 +113,32 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, /** * Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache]. + * + * @return a pair of the well known identity to contact for a signature, and the public key that party should sign + * with (this may belong to a confidential identity). */ - @Suspendable private fun keysToParties(keys: Collection): List = keys.map { - // TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key. - val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) + @Suspendable private fun keysToParties(keys: Collection): List> = keys.map { + val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it)) ?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.") - partyNode.legalIdentity + Pair(party, it) } // DOCSTART 1 /** * Get and check the required signature. + * + * @param counterparty the party to request a signature from. + * @param signingKey the key the party should use to sign the transaction. */ - @Suspendable private fun collectSignature(counterparty: Party): TransactionSignature { - // SendTransactionFlow allows otherParty to access our data to resolve the transaction. + @Suspendable private fun collectSignature(counterparty: Party, signingKey: PublicKey): TransactionSignature { + // SendTransactionFlow allows counterparty to access our data to resolve the transaction. subFlow(SendTransactionFlow(counterparty, partiallySignedTx)) + // Send the key we expect the counterparty to sign with - this is important where they may have several + // keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward + // for us to check we have the expected signature returned. + send(counterparty, signingKey) return receive(counterparty).unwrap { - require(counterparty.owningKey.isFulfilledBy(it.by)) { "Not signed by the required Party." } + require(signingKey.isFulfilledBy(it.by)) { "Not signed by the required signing key." } it } } @@ -189,9 +199,16 @@ abstract class SignTransactionFlow(val otherParty: Party, progressTracker.currentStep = RECEIVING // Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures. val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false)) + // Receive the signing key that the party requesting the signature expects us to sign with. Having this provided + // means we only have to check we own that one key, rather than matching all keys in the transaction against all + // keys we own. + val signingKey = receive(otherParty).unwrap { + // TODO: We should have a faster way of verifying we own a single key + serviceHub.keyManagementService.filterMyKeys(listOf(it)).single() + } progressTracker.currentStep = VERIFYING // Check that the Responder actually needs to sign. - checkMySignatureRequired(stx) + checkMySignatureRequired(stx, signingKey) // Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's. checkSignatures(stx) stx.tx.toLedgerTransaction(serviceHub).verify() @@ -206,7 +223,7 @@ abstract class SignTransactionFlow(val otherParty: Party, } // Sign and send back our signature to the Initiator. progressTracker.currentStep = SIGNING - val mySignature = serviceHub.createSignature(stx) + val mySignature = serviceHub.createSignature(stx, signingKey) send(otherParty, mySignature) // Return the fully signed transaction once it has been committed. @@ -214,8 +231,10 @@ abstract class SignTransactionFlow(val otherParty: Party, } @Suspendable private fun checkSignatures(stx: SignedTransaction) { - require(stx.sigs.any { it.by == otherParty.owningKey }) { - "The Initiator of CollectSignaturesFlow must have signed the transaction." + val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey) + val signingWellKnownIdentities = signingIdentities.mapNotNull(serviceHub.identityService::partyFromAnonymous) + require(otherParty in signingWellKnownIdentities) { + "The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherParty}" } val signed = stx.sigs.map { it.by } val allSigners = stx.tx.requiredSigningKeys @@ -245,10 +264,8 @@ abstract class SignTransactionFlow(val otherParty: Party, */ @Suspendable abstract protected fun checkTransaction(stx: SignedTransaction) - @Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) { - // TODO: Revisit when key management is properly fleshed out. - val myKey = serviceHub.myInfo.legalIdentity.owningKey - require(myKey in stx.tx.requiredSigningKeys) { + @Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) { + require(signingKey in stx.tx.requiredSigningKeys) { "Party is not a participant for any of the input states of transaction ${stx.id}" } } diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 769de29fb4..09a9ae33a3 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -3,13 +3,14 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Command import net.corda.core.contracts.requireThat -import net.corda.testing.contracts.DummyContract +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.getOrThrow import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.testing.MINI_CORP_KEY +import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockServices import org.junit.After @@ -77,16 +78,18 @@ class CollectSignaturesFlowTests { } @InitiatedBy(TestFlow.Initiator::class) - class Responder(val otherParty: Party) : FlowLogic() { + class Responder(val otherParty: Party, val identities: Map) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { val state = receive(otherParty).unwrap { it } val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity - val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey }) + val myInputKeys = state.participants.map { it.owningKey } + val myKeys = myInputKeys + (identities[serviceHub.myInfo.legalIdentity] ?: serviceHub.myInfo.legalIdentity).owningKey + val command = Command(DummyContract.Commands.Create(), myInputKeys) val builder = TransactionBuilder(notary).withItems(state, command) val ptx = serviceHub.signInitialTransaction(builder) - val stx = subFlow(CollectSignaturesFlow(ptx)) + val stx = subFlow(CollectSignaturesFlow(ptx, myKeys)) val ftx = subFlow(FinalityFlow(stx)).single() return ftx @@ -103,10 +106,11 @@ class CollectSignaturesFlowTests { @Suspendable override fun call(): SignedTransaction { val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity - val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey }) + val myInputKeys = state.participants.map { it.owningKey } + val command = Command(DummyContract.Commands.Create(), myInputKeys) val builder = TransactionBuilder(notary).withItems(state, command) val ptx = serviceHub.signInitialTransaction(builder) - val stx = subFlow(CollectSignaturesFlow(ptx)) + val stx = subFlow(CollectSignaturesFlow(ptx, myInputKeys)) val ftx = subFlow(FinalityFlow(stx)).single() return ftx @@ -136,9 +140,12 @@ class CollectSignaturesFlowTests { @Test fun `successfully collects two signatures`() { + val bConfidentialIdentity = b.services.keyManagementService.freshKeyAndCert(b.info.legalIdentityAndCert, false) + // Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity + a.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity) registerFlowOnAllNodes(TestFlowTwo.Responder::class) val magicNumber = 1337 - val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity) + val parties = listOf(a.info.legalIdentity, bConfidentialIdentity.party, c.info.legalIdentity) val state = DummyContract.MultiOwnerState(magicNumber, parties) val flow = a.services.startFlow(TestFlowTwo.Initiator(state)) mockNet.runNetwork() diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 7d312b50ea..495e559206 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -325,6 +325,11 @@ class TwoPartyTradeFlowTests { val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) val issuer = bankNode.info.legalIdentity.ref(1, 2, 3) + val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode) + allNodes.forEach { node -> + allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.verifyAndRegisterIdentity(identity) } + } + ledger(aliceNode.services, initialiseSerialization = false) { // Insert a prospectus type attachment into the commercial paper transaction. diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index f37590f560..c18dac37b2 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -43,6 +43,11 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>()) override fun startMainSimulation(): CordaFuture { + // TODO: Determine why this isn't happening via the network map + mockNet.nodes.map { it.services.identityService }.forEach { service -> + mockNet.nodes.forEach { node -> service.registerIdentity(node.info.legalIdentityAndCert) } + } + val future = openFuture() om = JacksonSupport.createInMemoryMapper(InMemoryIdentityService((banks + regulators + networkMap).map { it.info.legalIdentityAndCert }, trustRoot = DUMMY_CA.certificate)) registerFinanceJSONMappers(om)