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.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<SignedTransaction>() {
class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
val myOptionalKeys: Iterable<PublicKey>?,
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
@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<PublicKey> = 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<PublicKey>): List<Party> = 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<PublicKey>): List<Pair<Party, PublicKey>> = 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<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
}
}
@ -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<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
// 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}"
}
}

View File

@ -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<SignedTransaction>() {
class Responder(val otherParty: Party, val identities: Map<Party, AnonymousParty>) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val state = receive<DummyContract.MultiOwnerState>(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()

View File

@ -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.

View File

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