Enable anonymisation in two party deal/trade flows

This commit is contained in:
Ross Nicoll 2017-08-23 12:02:35 +01:00
parent a84cd567d8
commit bc5aceddbf
5 changed files with 130 additions and 56 deletions

View File

@ -4,11 +4,9 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.requireThat
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.flows.CollectSignaturesFlow
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.SignTransactionFlow
import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.ServiceType
@ -29,9 +27,14 @@ import java.security.PublicKey
// TODO: Also, the term Deal is used here where we might prefer Agreement.
// TODO: Make this flow more generic.
object TwoPartyDealFlow {
// This object is serialised to the network and is the first flow message the seller sends to the buyer.
/**
* This object is serialised to the network and is the first flow message the seller sends to the buyer.
*
* @param primaryIdentity the (anonymised) identity of the participant that initiates communication/handshake.
* @param secondaryIdentity the (anonymised) identity of the participant that is recipient of initial communication.
*/
@CordaSerializable
data class Handshake<out T>(val payload: T, val publicKey: PublicKey)
data class Handshake<out T>(val payload: T, val primaryIdentity: AnonymousParty, val secondaryIdentity: AnonymousParty)
/**
* Abstracted bilateral deal flow participant that initiates communication/handshake.
@ -39,19 +42,25 @@ object TwoPartyDealFlow {
abstract class Primary(override val progressTracker: ProgressTracker = Primary.tracker()) : FlowLogic<SignedTransaction>() {
companion object {
object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities")
object SENDING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal.")
fun tracker() = ProgressTracker(SENDING_PROPOSAL)
fun tracker() = ProgressTracker(GENERATING_ID, SENDING_PROPOSAL)
}
abstract val payload: Any
abstract val notaryNode: NodeInfo
abstract val otherParty: Party
// TODO: This is never read from, and should be removed
abstract val myKey: PublicKey
@Suspendable override fun call(): SignedTransaction {
progressTracker.currentStep = GENERATING_ID
val txIdentities = subFlow(TransactionKeyFlow(otherParty))
val anonymousMe = txIdentities.get(serviceHub.myInfo.legalIdentity) ?: serviceHub.myInfo.legalIdentity.anonymise()
val anonymousCounterparty = txIdentities.get(otherParty) ?: otherParty.anonymise()
progressTracker.currentStep = SENDING_PROPOSAL
// Make the first message we'll send to kick off the flow.
val hello = Handshake(payload, serviceHub.myInfo.legalIdentity.owningKey)
val hello = Handshake(payload, anonymousMe, anonymousCounterparty)
// Wait for the FinalityFlow to finish on the other side and return the tx when it's available.
send(otherParty, hello)
@ -105,7 +114,7 @@ object TwoPartyDealFlow {
progressTracker.currentStep = COLLECTING_SIGNATURES
// DOCSTART 1
val stx = subFlow(CollectSignaturesFlow(ptx))
val stx = subFlow(CollectSignaturesFlow(ptx, additionalSigningPubKeys))
// DOCEND 1
logger.trace { "Got signatures from other party, verifying ... " }
@ -138,7 +147,14 @@ object TwoPartyDealFlow {
val handshake = receive<Handshake<U>>(otherParty)
progressTracker.currentStep = VERIFYING
return handshake.unwrap { validateHandshake(it) }
return handshake.unwrap {
// Verify the transaction identities represent the correct parties
val wellKnownOtherParty = serviceHub.identityService.partyFromAnonymous(it.primaryIdentity)
val wellKnownMe = serviceHub.identityService.partyFromAnonymous(it.secondaryIdentity)
require(wellKnownOtherParty == otherParty)
require(wellKnownMe == serviceHub.myInfo.legalIdentity)
validateHandshake(it)
}
}
@Suspendable protected abstract fun validateHandshake(handshake: Handshake<U>): Handshake<U>
@ -155,7 +171,6 @@ object TwoPartyDealFlow {
override val payload: AutoOffer,
override val myKey: PublicKey,
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary() {
override val notaryNode: NodeInfo get() =
serviceHub.networkMapCache.notaryNodes.filter { it.notaryIdentity == payload.notary }.single()

View File

@ -5,10 +5,10 @@ import net.corda.core.contracts.Amount
import net.corda.core.contracts.OwnableState
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.withoutIssuer
import net.corda.core.contracts.*
import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
@ -48,18 +48,22 @@ object TwoPartyTradeFlow {
override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
}
// This object is serialised to the network and is the first flow message the seller sends to the buyer.
/**
* This object is serialised to the network and is the first flow message the seller sends to the buyer.
*
* @param payToIdentity anonymous identity of the seller, for payment to be sent to.
*/
@CordaSerializable
data class SellerTradeInfo(
val price: Amount<Currency>,
val sellerOwner: AbstractParty
val payToIdentity: PartyAndCertificate
)
open class Seller(val otherParty: Party,
val notaryNode: NodeInfo,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount<Currency>,
val me: AbstractParty,
val me: PartyAndCertificate,
override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic<SignedTransaction>() {
companion object {
@ -84,12 +88,26 @@ object TwoPartyTradeFlow {
// SendTransactionFlow allows otherParty to access our data to resolve the transaction.
subFlow(SendStateAndRefFlow(otherParty, listOf(assetToSell)))
send(otherParty, hello)
// Verify and sign the transaction.
progressTracker.currentStep = VERIFYING_AND_SIGNING
// Sync identities to ensure we know all of the identities involved in the transaction we're about to
// be asked to sign
subFlow(IdentitySyncFlow.Receive(otherParty))
// DOCSTART 5
val signTransactionFlow = object : SignTransactionFlow(otherParty, VERIFYING_AND_SIGNING.childProgressTracker()) {
override fun checkTransaction(stx: SignedTransaction) {
if (stx.tx.outputStates.sumCashBy(me).withoutIssuer() != price)
// Verify that we know who all the participants in the transaction are
val states: Iterable<ContractState> = (stx.tx.inputs.map { serviceHub.loadState(it).data } + stx.tx.outputs.map { it.data })
states.forEach { state ->
state.participants.forEach { anon ->
require(serviceHub.identityService.partyFromAnonymous(anon) != null) { "Transaction state ${state} involves unknown participant ${anon}" }
}
}
if (stx.tx.outputStates.sumCashBy(me.party).withoutIssuer() != price)
throw FlowException("Transaction is not sending us the right amount of cash")
}
}
@ -114,7 +132,9 @@ object TwoPartyTradeFlow {
open class Buyer(val otherParty: Party,
val notary: Party,
val acceptablePrice: Amount<Currency>,
val typeToBuy: Class<out OwnableState>) : FlowLogic<SignedTransaction>() {
val typeToBuy: Class<out OwnableState>,
val anonymous: Boolean) : FlowLogic<SignedTransaction>() {
constructor(otherParty: Party, notary: Party, acceptablePrice: Amount<Currency>, typeToBuy: Class<out OwnableState>): this(otherParty, notary, acceptablePrice, typeToBuy, true)
// DOCSTART 2
object RECEIVING : ProgressTracker.Step("Waiting for seller trading info")
@ -139,16 +159,27 @@ object TwoPartyTradeFlow {
progressTracker.currentStep = RECEIVING
val (assetForSale, tradeRequest) = receiveAndValidateTradeRequest()
// Create the identity we'll be paying to, and send the counterparty proof we own the identity
val buyerAnonymousIdentity = if (anonymous)
serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false)
else
serviceHub.myInfo.legalIdentityAndCert
// Put together a proposed transaction that performs the trade, and sign it.
progressTracker.currentStep = SIGNING
val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest)
val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest, buyerAnonymousIdentity)
// Now sign the transaction with whatever keys we need to move the cash.
val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys)
// Sync up confidential identities in the transaction with our counterparty
subFlow(IdentitySyncFlow.Send(otherParty, ptx.toWireTransaction()))
// Send the signed transaction to the seller, who must then sign it themselves and commit
// it to the ledger by sending it to the notary.
progressTracker.currentStep = COLLECTING_SIGNATURES
val twiceSignedTx = subFlow(CollectSignaturesFlow(partSignedTx, COLLECTING_SIGNATURES.childProgressTracker()))
val twiceSignedTx = subFlow(CollectSignaturesFlow(partSignedTx, cashSigningPubKeys, COLLECTING_SIGNATURES.childProgressTracker()))
// Notarise and record the transaction.
progressTracker.currentStep = RECORDING
return subFlow(FinalityFlow(twiceSignedTx)).single()
@ -159,33 +190,40 @@ object TwoPartyTradeFlow {
val assetForSale = subFlow(ReceiveStateAndRefFlow<OwnableState>(otherParty)).single()
return assetForSale to receive<SellerTradeInfo>(otherParty).unwrap {
progressTracker.currentStep = VERIFYING
// What is the seller trying to sell us?
val asset = assetForSale.state.data
val assetTypeName = asset.javaClass.name
// The asset must either be owned by the well known identity of the counterparty, or we must be able to
// prove the owner is a confidential identity of the counterparty.
val assetForSaleIdentity = serviceHub.identityService.partyFromAnonymous(asset.owner)
require(assetForSaleIdentity == otherParty)
// Register the identity we're about to send payment to. This shouldn't be the same as the asset owner
// identity, so that anonymity is enforced.
val wellKnownPayToIdentity = serviceHub.identityService.verifyAndRegisterIdentity(it.payToIdentity)
require(wellKnownPayToIdentity?.party == otherParty) { "Well known identity to pay to must match counterparty identity" }
if (it.price > acceptablePrice)
throw UnacceptablePriceException(it.price)
if (!typeToBuy.isInstance(asset))
throw AssetMismatchException(typeToBuy.name, assetTypeName)
it
}
}
@Suspendable
private fun assembleSharedTX(assetForSale: StateAndRef<OwnableState>, tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
private fun assembleSharedTX(assetForSale: StateAndRef<OwnableState>, tradeRequest: SellerTradeInfo, buyerAnonymousIdentity: PartyAndCertificate): SharedTx {
val ptx = TransactionBuilder(notary)
// Add input and output states for the movement of cash, by using the Cash contract to generate the states
val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, tradeRequest.sellerOwner)
val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, tradeRequest.payToIdentity.party)
// Add inputs/outputs/a command for the movement of the asset.
tx.addInputState(assetForSale)
// Just pick some new public key for now. This won't be linked with our identity in any way, which is what
// we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to
// reveal who the owner actually is. The key management service is expected to derive a unique key from some
// initial seed in order to provide privacy protection.
val freshKey = serviceHub.keyManagementService.freshKey()
val (command, state) = assetForSale.state.data.withNewOwner(AnonymousParty(freshKey))
val (command, state) = assetForSale.state.data.withNewOwner(buyerAnonymousIdentity.party)
tx.addOutputState(state, assetForSale.state.notary)
tx.addCommand(command, assetForSale.state.data.owner.owningKey)
@ -193,8 +231,11 @@ object TwoPartyTradeFlow {
// But it can't hurt to have one.
val currentTime = serviceHub.clock.instant()
tx.setTimeWindow(currentTime, 30.seconds)
return Pair(tx, cashSigningPubKeys)
return SharedTx(tx, cashSigningPubKeys)
}
// DOCEND 1
data class SharedTx(val tx: TransactionBuilder, val cashSigningPubKeys: List<PublicKey>)
}
}

View File

@ -11,6 +11,7 @@ import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.concurrent.map
import net.corda.core.internal.rootCause
@ -20,6 +21,7 @@ import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.Vault
import net.corda.core.serialization.CordaSerializable
import net.corda.core.toFuture
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
@ -145,19 +147,18 @@ class TwoPartyTradeFlowTests {
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name)
val cashIssuer = bankNode.info.legalIdentity.ref(1)
val cpIssuer = bankNode.info.legalIdentity.ref(1, 2, 3)
val issuer = bankNode.info.legalIdentity.ref(1)
aliceNode.disableDBCloseOnStop()
bobNode.disableDBCloseOnStop()
val cashStates = bobNode.database.transaction {
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notaryNode.info.notaryIdentity, 3, 3,
issuedBy = cashIssuer)
issuedBy = issuer)
}
val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity,
fillUpForSeller(false, issuer, aliceNode.info.legalIdentity,
1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), null, notaryNode.info.notaryIdentity).second
}
@ -199,8 +200,13 @@ class TwoPartyTradeFlowTests {
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name)
val cashIssuer = bankNode.info.legalIdentity.ref(1)
val cpIssuer = bankNode.info.legalIdentity.ref(1, 2, 3)
val issuer = bankNode.info.legalIdentity.ref(1, 2, 3)
// Let the nodes know about each other - normally the network map would handle this
val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode)
allNodes.forEach { node ->
allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.registerIdentity(identity) }
}
aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert)
bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert)
@ -214,10 +220,10 @@ class TwoPartyTradeFlowTests {
bobNode.database.transaction {
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notaryNode.info.notaryIdentity,
issuedBy = cashIssuer)
issuedBy = issuer)
}
val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity,
fillUpForSeller(false, issuer, aliceNode.info.legalIdentity,
1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), null, notaryNode.info.notaryIdentity).second
}
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
@ -253,6 +259,9 @@ class TwoPartyTradeFlowTests {
// She will wait around until Bob comes back.
assertThat(aliceNode.pumpReceive()).isNotNull()
// FIXME: Knowledge of confidential identities is lost on node shutdown, so Bob's node now refuses to sign the
// transaction because it has no idea who the parties are.
// ... bring the node back up ... the act of constructing the SMM will re-register the message handlers
// that Bob was waiting on before the reboot occurred.
bobNode = mockNet.createNode(networkMapAddress, bobAddr.id, object : MockNetwork.Factory<MockNetwork.MockNode> {
@ -437,7 +446,6 @@ class TwoPartyTradeFlowTests {
}
ledger(aliceNode.services, initialiseSerialization = false) {
// Insert a prospectus type attachment into the commercial paper transaction.
val stream = ByteArrayOutputStream()
JarOutputStream(stream).use {
@ -529,13 +537,11 @@ class TwoPartyTradeFlowTests {
private fun runBuyerAndSeller(notaryNode: MockNetwork.MockNode,
sellerNode: MockNetwork.MockNode,
buyerNode: MockNetwork.MockNode,
assetToSell: StateAndRef<OwnableState>): RunResult {
val anonymousSeller = sellerNode.services.let { serviceHub ->
serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false)
}.party.anonymise()
val buyerFlows: Observable<BuyerAcceptor> = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java)
assetToSell: StateAndRef<OwnableState>,
anonymous: Boolean = true): RunResult {
val buyerFlows: Observable<out FlowLogic<*>> = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java)
val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine }
val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS, anonymousSeller)
val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS, anonymous)
val sellerResult = sellerNode.services.startFlow(seller).resultFuture
return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id)
}
@ -545,10 +551,15 @@ class TwoPartyTradeFlowTests {
val notary: NodeInfo,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount<Currency>,
val me: AnonymousParty) : FlowLogic<SignedTransaction>() {
val anonymous: Boolean) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
send(buyer, Pair(notary.notaryIdentity, price))
val me = if (anonymous) {
serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false)
} else {
serviceHub.myInfo.legalIdentityAndCert
}
send(buyer, TestTx(notary.notaryIdentity, price, anonymous))
return subFlow(Seller(
buyer,
notary,
@ -562,14 +573,17 @@ class TwoPartyTradeFlowTests {
class BuyerAcceptor(val seller: Party) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val (notary, price) = receive<Pair<Party, Amount<Currency>>>(seller).unwrap {
require(serviceHub.networkMapCache.isNotary(it.first)) { "${it.first} is not a notary" }
val (notary, price, anonymous) = receive<TestTx>(seller).unwrap {
require(serviceHub.networkMapCache.isNotary(it.notaryIdentity)) { "${it.notaryIdentity} is not a notary" }
it
}
return subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java))
return subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java, anonymous))
}
}
@CordaSerializable
data class TestTx(val notaryIdentity: Party, val price: Amount<Currency>, val anonymous: Boolean)
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.runWithError(
bobError: Boolean,
aliceError: Boolean,
@ -581,6 +595,12 @@ class TwoPartyTradeFlowTests {
val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name)
val issuer = bankNode.info.legalIdentity.ref(1, 2, 3)
// Let the nodes know about each other - normally the network map would handle this
val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode)
allNodes.forEach { node ->
allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.registerIdentity(identity) }
}
val bobsBadCash = bobNode.database.transaction {
fillUpForBuyer(bobError, issuer, bobNode.info.legalIdentity,
notaryNode.info.notaryIdentity).second

View File

@ -8,6 +8,7 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.SchedulableFlow
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.ServiceType
@ -110,8 +111,7 @@ object FixingFlow {
}
override val myKey: PublicKey get() {
dealToFix.state.data.participants.single { it.owningKey == serviceHub.myInfo.legalIdentity.owningKey }
return serviceHub.legalIdentityKey
return serviceHub.keyManagementService.filterMyKeys(dealToFix.state.data.participants.map(AbstractParty::owningKey)).single()
}
override val notaryNode: NodeInfo get() {

View File

@ -6,7 +6,6 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo
import net.corda.core.transactions.SignedTransaction
@ -42,10 +41,9 @@ class SellerFlow(val otherParty: Party,
progressTracker.currentStep = SELF_ISSUING
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
val cpOwner = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, false)
val commercialPaper = serviceHub.vaultQueryService.queryBy(CommercialPaper.State::class.java).states.first()
progressTracker.currentStep = TRADING
// Send the offered amount.
@ -55,7 +53,7 @@ class SellerFlow(val otherParty: Party,
notary,
commercialPaper,
amount,
AnonymousParty(cpOwnerKey),
cpOwner,
progressTracker.getChildProgressTracker(TRADING)!!)
return subFlow(seller)
}