mirror of
https://github.com/corda/corda.git
synced 2025-05-02 08:43:15 +00:00
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:
parent
d0a3aa3fc7
commit
65c5ce65a6
@ -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}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user