mirror of
https://github.com/corda/corda.git
synced 2025-02-23 10:30:24 +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.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}"
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user