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.
This commit is contained in:
Ross Nicoll 2017-08-24 11:19:34 +01:00 committed by GitHub
parent d0a3aa3fc7
commit 65c5ce65a6
4 changed files with 66 additions and 32 deletions

View File

@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -56,12 +57,14 @@ import java.security.PublicKey
* val stx = subFlow(CollectSignaturesFlow(ptx)) * val stx = subFlow(CollectSignaturesFlow(ptx))
* *
* @param partiallySignedTx Transaction to collect the remaining signatures for * @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: AbstractStateReplacementFlow needs updating to use this flow.
// TODO: Update this flow to handle randomly generated keys when that work is complete. class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, val myOptionalKeys: Iterable<PublicKey>?,
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() { override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, null, progressTracker)
companion object { companion object {
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.") object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.") object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
@ -72,16 +75,14 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
} }
@Suspendable override fun call(): 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. // 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. // Usually just the Initiator and possibly an oracle would have signed at this point.
val myKey = serviceHub.myInfo.legalIdentity.owningKey val myKeys: Iterable<PublicKey> = myOptionalKeys ?: listOf(serviceHub.myInfo.legalIdentity.owningKey)
val signed = partiallySignedTx.sigs.map { it.by } val signed = partiallySignedTx.sigs.map { it.by }
val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed
// One of the signatures collected so far MUST be from the initiator of this flow. // 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." "The Initiator of CollectSignaturesFlow must have signed the transaction."
} }
@ -100,7 +101,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
if (unsigned.isEmpty()) return partiallySignedTx if (unsigned.isEmpty()) return partiallySignedTx
// Collect signatures from all counter-parties and append them to the partially signed transaction. // 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 val stx = partiallySignedTx + counterpartySignatures
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures. // 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]. * 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<PublicKey>): List<Party> = keys.map { @Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Pair<Party, PublicKey>> = keys.map {
// TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key. val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it))
val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it)
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.") ?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
partyNode.legalIdentity Pair(party, it)
} }
// DOCSTART 1 // DOCSTART 1
/** /**
* Get and check the required signature. * 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 { @Suspendable private fun collectSignature(counterparty: Party, signingKey: PublicKey): TransactionSignature {
// SendTransactionFlow allows otherParty to access our data to resolve the transaction. // SendTransactionFlow allows counterparty to access our data to resolve the transaction.
subFlow(SendTransactionFlow(counterparty, partiallySignedTx)) 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<TransactionSignature>(counterparty).unwrap { return receive<TransactionSignature>(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 it
} }
} }
@ -189,9 +199,16 @@ abstract class SignTransactionFlow(val otherParty: Party,
progressTracker.currentStep = RECEIVING progressTracker.currentStep = RECEIVING
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures. // Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false)) 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<PublicKey>(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 progressTracker.currentStep = VERIFYING
// Check that the Responder actually needs to sign. // 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. // Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
checkSignatures(stx) checkSignatures(stx)
stx.tx.toLedgerTransaction(serviceHub).verify() stx.tx.toLedgerTransaction(serviceHub).verify()
@ -206,7 +223,7 @@ abstract class SignTransactionFlow(val otherParty: Party,
} }
// Sign and send back our signature to the Initiator. // Sign and send back our signature to the Initiator.
progressTracker.currentStep = SIGNING progressTracker.currentStep = SIGNING
val mySignature = serviceHub.createSignature(stx) val mySignature = serviceHub.createSignature(stx, signingKey)
send(otherParty, mySignature) send(otherParty, mySignature)
// Return the fully signed transaction once it has been committed. // 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) { @Suspendable private fun checkSignatures(stx: SignedTransaction) {
require(stx.sigs.any { it.by == otherParty.owningKey }) { val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey)
"The Initiator of CollectSignaturesFlow must have signed the transaction." 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 signed = stx.sigs.map { it.by }
val allSigners = stx.tx.requiredSigningKeys val allSigners = stx.tx.requiredSigningKeys
@ -245,10 +264,8 @@ abstract class SignTransactionFlow(val otherParty: Party,
*/ */
@Suspendable abstract protected fun checkTransaction(stx: SignedTransaction) @Suspendable abstract protected fun checkTransaction(stx: SignedTransaction)
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) { @Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) {
// TODO: Revisit when key management is properly fleshed out. require(signingKey in stx.tx.requiredSigningKeys) {
val myKey = serviceHub.myInfo.legalIdentity.owningKey
require(myKey in stx.tx.requiredSigningKeys) {
"Party is not a participant for any of the input states of transaction ${stx.id}" "Party is not a participant for any of the input states of transaction ${stx.id}"
} }
} }

View File

@ -3,13 +3,14 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Command import net.corda.core.contracts.Command
import net.corda.core.contracts.requireThat 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.identity.Party
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.getOrThrow
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.testing.MINI_CORP_KEY import net.corda.testing.MINI_CORP_KEY
import net.corda.testing.contracts.DummyContract
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import org.junit.After import org.junit.After
@ -77,16 +78,18 @@ class CollectSignaturesFlowTests {
} }
@InitiatedBy(TestFlow.Initiator::class) @InitiatedBy(TestFlow.Initiator::class)
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() { class Responder(val otherParty: Party, val identities: Map<Party, AnonymousParty>) : FlowLogic<SignedTransaction>() {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
val state = receive<DummyContract.MultiOwnerState>(otherParty).unwrap { it } val state = receive<DummyContract.MultiOwnerState>(otherParty).unwrap { it }
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity 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 builder = TransactionBuilder(notary).withItems(state, command)
val ptx = serviceHub.signInitialTransaction(builder) val ptx = serviceHub.signInitialTransaction(builder)
val stx = subFlow(CollectSignaturesFlow(ptx)) val stx = subFlow(CollectSignaturesFlow(ptx, myKeys))
val ftx = subFlow(FinalityFlow(stx)).single() val ftx = subFlow(FinalityFlow(stx)).single()
return ftx return ftx
@ -103,10 +106,11 @@ class CollectSignaturesFlowTests {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity 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 builder = TransactionBuilder(notary).withItems(state, command)
val ptx = serviceHub.signInitialTransaction(builder) val ptx = serviceHub.signInitialTransaction(builder)
val stx = subFlow(CollectSignaturesFlow(ptx)) val stx = subFlow(CollectSignaturesFlow(ptx, myInputKeys))
val ftx = subFlow(FinalityFlow(stx)).single() val ftx = subFlow(FinalityFlow(stx)).single()
return ftx return ftx
@ -136,9 +140,12 @@ class CollectSignaturesFlowTests {
@Test @Test
fun `successfully collects two signatures`() { 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) registerFlowOnAllNodes(TestFlowTwo.Responder::class)
val magicNumber = 1337 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 state = DummyContract.MultiOwnerState(magicNumber, parties)
val flow = a.services.startFlow(TestFlowTwo.Initiator(state)) val flow = a.services.startFlow(TestFlowTwo.Initiator(state))
mockNet.runNetwork() mockNet.runNetwork()

View File

@ -325,6 +325,11 @@ class TwoPartyTradeFlowTests {
val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name)
val issuer = bankNode.info.legalIdentity.ref(1, 2, 3) 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) { ledger(aliceNode.services, initialiseSerialization = false) {
// Insert a prospectus type attachment into the commercial paper transaction. // Insert a prospectus type attachment into the commercial paper transaction.

View File

@ -43,6 +43,11 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten
private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>()) private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>())
override fun startMainSimulation(): CordaFuture<Unit> { override fun startMainSimulation(): CordaFuture<Unit> {
// 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<Unit>() val future = openFuture<Unit>()
om = JacksonSupport.createInMemoryMapper(InMemoryIdentityService((banks + regulators + networkMap).map { it.info.legalIdentityAndCert }, trustRoot = DUMMY_CA.certificate)) om = JacksonSupport.createInMemoryMapper(InMemoryIdentityService((banks + regulators + networkMap).map { it.info.legalIdentityAndCert }, trustRoot = DUMMY_CA.certificate))
registerFinanceJSONMappers(om) registerFinanceJSONMappers(om)