Refactor the 2-party trading protocol

- Fix a security bug/TODO by having seller send back the signatures rather than a full blown transaction (which would allow a malicious seller to try and confuse the buyer by sending back a completely different TX to the one he proposed)
- Introduce an UntrustworthyData<T> wrapper as an (inefficient) form of taint tracking, to make it harder to forget that data has come from an untrustworthy source and may be malicious.
- Split the giant {Buyer, Seller}.call() methods into a set of smaller methods that make it easier to unit test various kinds of failure/skip bits in tests that aren't needed.
This commit is contained in:
Mike Hearn 2016-02-09 15:11:17 +01:00
parent 28daae5bd4
commit d98a3871da
6 changed files with 337 additions and 213 deletions

View File

@ -125,6 +125,16 @@ each side.
return buyer.resultFuture
}
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
class SellerTradeInfo(
val assetForSale: StateAndRef<OwnableState>,
val price: Amount,
val sellerOwnerKey: PublicKey,
val sessionID: Long
)
class SignaturesFromSeller(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey)
class Seller(val otherSide: SingleMessageRecipient,
val timestampingAuthority: LegallyIdentifiableNode,
val assetToSell: StateAndRef<OwnableState>,
@ -137,21 +147,11 @@ each side.
}
}
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
private class SellerTradeInfo(
val assetForSale: StateAndRef<OwnableState>,
val price: Amount,
val sellerOwnerKey: PublicKey,
val sessionID: Long
)
class UnacceptablePriceException(val givenPrice: Amount) : Exception()
class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() {
override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
}
// The buyer's side of the protocol. See note above Seller to learn about the caveats here.
class Buyer(val otherSide: SingleMessageRecipient,
val timestampingAuthority: Party,
val acceptablePrice: Amount,
@ -167,7 +167,7 @@ each side.
Let's unpack what this code does:
- It defines a several classes nested inside the main ``TwoPartyTradeProtocol`` singleton, and a couple of methods, one
to run the buyer side of the protocol and one to run the seller side.
to run the buyer side of the protocol and one to run the seller side. Some of the classes are simply protocol messages.
- It defines the "trade topic", which is just a string that namespaces this protocol. The prefix "platform." is reserved
by the DLG, but you can define your own protocols using standard Java-style reverse DNS notation.
- The ``runBuyer`` and ``runSeller`` methods take a number of parameters that specialise the protocol for this run,
@ -205,7 +205,8 @@ to either runBuyer or runSeller, depending on who we are, and then call ``.get()
block the calling thread until the protocol has finished. Or we could register a callback on the returned future that
will be invoked when it's done, where we could e.g. update a user interface.
Finally, we define a couple of exceptions, and a class that will be used as a protocol message called ``SellerTradeInfo``.
Finally, we define a couple of exceptions, and two classes that will be used as a protocol message called
``SellerTradeInfo`` and ``SignaturesFromSeller``.
Suspendable methods
-------------------
@ -244,13 +245,57 @@ Let's implement the ``Seller.call`` method. This will be invoked by the platform
.. sourcecode:: kotlin
val sessionID = random63BitValue()
val partialTX: SignedWireTransaction = receiveAndCheckProposedTransaction()
// Make the first message we'll send to kick off the protocol.
val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID)
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
val ourSignature = signWithOurKey(partialTX)
val tsaSig = timestamp(partialTX)
val partialTX = sendAndReceive(TRADE_TOPIC, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java)
logger().trace { "Received partially signed transaction" }
val ledgerTX = sendSignatures(partialTX, ourSignature, tsaSig)
return Pair(partialTX.tx, ledgerTX)
Here we see the outline of the procedure. We receive a proposed trade transaction from the buyer and check that it's
valid. Then we sign with our own key, request a timestamping authority to assert with another signature that the
timestamp in the transaction (if any) is valid, and finally we send back both our signature and the TSA's signature.
Finally, we hand back to the code that invoked the protocol the finished transaction in a couple of different forms.
Let's fill out the ``receiveAndCheckProposedTransaction()`` method.
.. container:: codeset
.. sourcecode:: kotlin
@Suspendable
open fun receiveAndCheckProposedTransaction(): SignedWireTransaction {
val sessionID = random63BitValue()
// Make the first message we'll send to kick off the protocol.
val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID)
val maybePartialTX = sendAndReceive(TRADE_TOPIC, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java)
val partialTX = maybePartialTX.validate {
it.verifySignatures()
logger.trace { "Received partially signed transaction" }
val wtx: WireTransaction = it.tx
requireThat {
"transaction sends us the right amount of cash" by (wtx.outputs.sumCashBy(myKeyPair.public) == price)
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
//
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
// we're reusing keys! So don't reuse keys!
// - This tx may not be valid according to the contracts of the input states, so we must resolve
// and fully audit the transaction chains to convince ourselves that it is actually valid.
// - This tx may include output states that impose odd conditions on the movement of the cash,
// once we implement state pairing.
//
// but the goal of this code is not to be fully secure, but rather, just to find good ways to
// express protocol state machines on top of the messaging layer.
}
}
return partialTX
}
That's pretty straightforward. We generate a session ID to identify what's happening on the seller side, fill out
the initial protocol message, and then call ``sendAndReceive``. This function takes a few arguments:
@ -260,6 +305,11 @@ the initial protocol message, and then call ``sendAndReceive``. This function ta
- The thing to send. It'll be serialised and sent automatically.
- Finally a type argument, which is the kind of object we're expecting to receive from the other side.
It returns a simple wrapper class, ``UntrustworthyData<SignedWireTransaction>``, which is just a marker class that reminds
us that the data came from a potentially malicious external source and may have been tampered with or be unexpected in
other ways. It doesn't add any functionality, but acts as a reminder to "scrub" the data before use. Here, our scrubbing
simply involves checking the signatures on it. Then we could go ahead and do some more involved checks.
Once sendAndReceive is called, the call method will be suspended into a continuation. When it gets back we'll do a log
message. The buyer is supposed to send us a transaction with all the right inputs/outputs/commands in return, with their
cash put into the transaction and their signature on it authorising the movement of the cash.
@ -273,51 +323,45 @@ cash put into the transaction and their signature on it authorising the movement
doing things like creating threads from inside these calls would be a bad idea. They should only contain business
logic.
OK, let's keep going:
Here's the rest of the code:
.. container:: codeset
.. sourcecode:: kotlin
partialTX.verifySignatures()
val wtx = partialTX.txBits.deserialize<WireTransaction>()
open fun signWithOurKey(partialTX: SignedWireTransaction) = myKeyPair.signWithECDSA(partialTX.txBits)
requireThat {
"transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price)
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
//
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
// we're reusing keys! So don't reuse keys!
// - This tx may not be valid according to the contracts of the input states, so we must resolve
// and fully audit the transaction chains to convince ourselves that it is actually valid.
// - This tx may include output states that impose odd conditions on the movement of the cash,
// once we implement state pairing.
@Suspendable
open fun timestamp(partialTX: SignedWireTransaction): DigitalSignature.LegallyIdentifiable {
return TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits)
}
val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits.bits)
val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature)
fullySigned.verify()
val timestamped: TimestampedWireTransaction = fullySigned.toTimestampedTransaction(serviceHub.timestampingService)
logger().trace { "Built finished transaction, sending back to secondary!" }
@Suspendable
open fun sendSignatures(partialTX: SignedWireTransaction, ourSignature: DigitalSignature.WithKey,
tsaSig: DigitalSignature.LegallyIdentifiable): LedgerTransaction {
val fullySigned = partialTX + tsaSig + ourSignature
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
send(TRADE_TOPIC, sessionID, timestamped)
// TODO: We should run it through our full TransactionGroup of all transactions here.
return Pair(timestamped, timestamped.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService))
logger.trace { "Built finished transaction, sending back to secondary!" }
Here, we see some assertions and signature checking to satisfy ourselves that we're not about to sign something
incorrect. Once we're happy, we calculate a signature over the transaction to authorise the movement of the asset
we are selling, and then we verify things to make sure it's all OK. Finally, we request timestamping of the
transaction, in case the contracts governing the asset we're selling require it, and send the now finalised and
validated transaction back to the buyer.
send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(tsaSig, ourSignature))
return ltx
}
It's should be all pretty straightforward: here, ``txBits`` is the raw byte array representing the transaction.
In ``sendSignatures``, we take the two signatures we calculated, then add them to the partial transaction we were sent
and verify that the signatures all make sense. This should never fail: it's just a sanity check. Finally, we wrap the
two signatures in a simple wrapper message class and send it back. The send won't block waiting for an acknowledgement,
but the underlying message queue software will retry delivery if the other side has gone away temporarily.
.. warning:: This code is **not secure**. Other than not checking for all possible invalid constructions, if the
seller stops before sending the finalised transaction to the buyer, the seller is left with a valid transaction
but the buyer isn't, so they can't spend the asset they just purchased! This sort of thing will be fixed in a
future version of the code.
Finally, the call function returns with the result of the protocol: in our case, the final transaction in two different
forms.
Implementing the buyer
----------------------
@ -328,41 +372,54 @@ OK, let's do the same for the buyer side:
.. sourcecode:: kotlin
@Suspendable
override fun call(): Pair<TimestampedWireTransaction, LedgerTransaction> {
override fun call(): Pair<WireTransaction, LedgerTransaction> {
val tradeRequest = receiveAndValidateTradeRequest()
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID)
logger.trace { "Got signatures from seller, verifying ... "}
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
logger.trace { "Fully signed transaction was valid. Trade complete! :-)" }
return Pair(fullySigned.tx, ltx)
}
@Suspendable
open fun receiveAndValidateTradeRequest(): SellerTradeInfo {
// Wait for a trade request to come in on our pre-provided session ID.
val tradeRequest = receive(TRADE_TOPIC, args.sessionID, SellerTradeInfo::class.java)
val maybeTradeRequest = receive(TRADE_TOPIC, sessionID, SellerTradeInfo::class.java)
// What is the seller trying to sell us?
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
logger().trace { "Got trade request for a $assetTypeName" }
val tradeRequest = maybeTradeRequest.validate {
// What is the seller trying to sell us?
val assetTypeName = it.assetForSale.state.javaClass.name
logger.trace { "Got trade request for a $assetTypeName" }
// Check the start message for acceptability.
check(tradeRequest.sessionID > 0)
if (tradeRequest.price > acceptablePrice)
throw UnacceptablePriceException(tradeRequest.price)
if (!typeToBuy.isInstance(tradeRequest.assetForSale.state))
throw AssetMismatchException(typeToBuy.name, assetTypeName)
// Check the start message for acceptability.
check(it.sessionID > 0)
if (it.price > acceptablePrice)
throw UnacceptablePriceException(it.price)
if (!typeToBuy.isInstance(it.assetForSale.state))
throw AssetMismatchException(typeToBuy.name, assetTypeName)
}
// TODO: Either look up the stateref here in our local db, or accept a long chain
// of states and validate them to audit the other side and ensure it actually owns
// the state we are being offered! For now, just assume validity!
// TODO: Either look up the stateref here in our local db, or accept a long chain of states and
// validate them to audit the other side and ensure it actually owns the state we are being offered!
// For now, just assume validity!
return tradeRequest
}
// Generate the shared transaction that both sides will sign, using the data we have.
val ptx = TransactionBuilder()
// Add input and output states for the movement of cash, by using the Cash contract
// to generate the states.
val wallet = serviceHub.walletService.currentWallet
val cashStates = wallet.statesOfType<Cash.State>()
val cashSigningPubKeys = Cash().craftSpend(ptx, tradeRequest.price,
tradeRequest.sellerOwnerKey, cashStates)
// Add inputs/outputs/a command for the movement of the asset.
ptx.addInputState(tradeRequest.assetForSale.ref)
// Just pick some new public key for now.
val freshKey = serviceHub.keyManagementService.freshKey()
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
ptx.addOutputState(state)
ptx.addArg(WireCommand(command, tradeRequest.assetForSale.state.owner))
@Suspendable
open fun swapSignaturesWithSeller(stx: SignedWireTransaction, theirSessionID: Long): SignaturesFromSeller {
logger.trace { "Sending partially signed transaction to seller" }
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
return sendAndReceive(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx, SignaturesFromSeller::class.java).validate {}
}
open fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedWireTransaction {
// Now sign the transaction with whatever keys we need to move the cash.
for (k in cashSigningPubKeys) {
val priv = serviceHub.keyManagementService.toPrivate(k)
@ -374,36 +431,43 @@ OK, let's do the same for the buyer side:
// TODO: Could run verify() here to make sure the only signature missing is the sellers.
logger().trace { "Sending partially signed transaction to seller" }
return stx
}
// TODO: Protect against the buyer terminating here and leaving us in the lurch without
// the final tx.
// TODO: Protect against a malicious buyer sending us back a different transaction to
// the one we built.
val fullySigned = sendAndReceive(TRADE_TOPIC, tradeRequest.sessionID, sessionID, stx,
TimestampedWireTransaction::class.java)
open fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
val ptx = TransactionBuilder()
// Add input and output states for the movement of cash, by using the Cash contract to generate the states.
val wallet = serviceHub.walletService.currentWallet
val cashStates = wallet.statesOfType<Cash.State>()
val cashSigningPubKeys = Cash().generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates)
// Add inputs/outputs/a command for the movement of the asset.
ptx.addInputState(tradeRequest.assetForSale.ref)
// 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) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
ptx.addOutputState(state)
ptx.addCommand(command, tradeRequest.assetForSale.state.owner)
logger().trace { "Got fully signed transaction, verifying ... "}
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService,
serviceHub.identityService)
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
return Pair(fullySigned, ltx)
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
// to have one.
ptx.setTime(Instant.now(), timestampingAuthority, 30.seconds)
return Pair(ptx, cashSigningPubKeys)
}
This code is longer but still fairly straightforward. Here are some things to pay attention to:
1. We do some sanity checking on the received message to ensure we're being offered what we expected to be offered.
2. We create a cash spend in the normal way, by using ``Cash().craftSpend``. See the contracts tutorial if this isn't
2. We create a cash spend in the normal way, by using ``Cash().generateSpend``. See the contracts tutorial if this isn't
clear.
3. We access the *service hub* when we need it to access things that are transient and may change or be recreated
whilst a protocol is suspended, things like the wallet or the timestamping service. Remember that a protocol may
be suspended when it waits to receive a message across node or computer restarts, so objects representing a service
or data which may frequently change should be accessed 'just in time'.
4. Finally, we send the unfinsished, invalid transaction to the seller so they can sign it. They are expected to send
back to us a ``TimestampedWireTransaction``, which once we verify it, should be the final outcome of the trade.
4. Finally, we send the unfinished, invalid transaction to the seller so they can sign it. They are expected to send
back to us a ``SignaturesFromSeller``, which once we verify it, should be the final outcome of the trade.
As you can see, the protocol logic is straightforward and does not contain any callbacks or network glue code, despite
the fact that it takes minimal resources and can survive node restarts.

View File

@ -13,13 +13,13 @@ import com.google.common.util.concurrent.ListenableFuture
import contracts.Cash
import contracts.sumCashBy
import core.*
import core.crypto.DigitalSignature
import core.crypto.signWithECDSA
import core.messaging.LegallyIdentifiableNode
import core.messaging.ProtocolStateMachine
import core.messaging.SingleMessageRecipient
import core.messaging.StateMachineManager
import core.node.TimestamperClient
import core.serialization.deserialize
import core.utilities.trace
import java.security.KeyPair
import java.security.PublicKey
@ -67,98 +67,162 @@ object TwoPartyTradeProtocol {
return buyer.resultFuture
}
class Seller(val otherSide: SingleMessageRecipient,
val timestampingAuthority: LegallyIdentifiableNode,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount,
val myKeyPair: KeyPair,
val buyerSessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
@Suspendable
override fun call(): Pair<WireTransaction, LedgerTransaction> {
val sessionID = random63BitValue()
// Make the first message we'll send to kick off the protocol.
val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID)
val partialTX = sendAndReceive(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java)
logger.trace { "Received partially signed transaction" }
partialTX.verifySignatures()
val wtx: WireTransaction = partialTX.txBits.deserialize()
requireThat {
"transaction sends us the right amount of cash" by (wtx.outputs.sumCashBy(myKeyPair.public) == price)
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
//
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
// we're reusing keys! So don't reuse keys!
// - This tx may not be valid according to the contracts of the input states, so we must resolve
// and fully audit the transaction chains to convince ourselves that it is actually valid.
// - This tx may include output states that impose odd conditions on the movement of the cash,
// once we implement state pairing.
//
// but the goal of this code is not to be fully secure, but rather, just to find good ways to
// express protocol state machines on top of the messaging layer.
}
// Sign with our key and get the timestamping authorities key as well.
// These two steps could be done in parallel, in theory.
val ourSignature = myKeyPair.signWithECDSA(partialTX.txBits)
val tsaSig = TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits)
val fullySigned = partialTX.withAdditionalSignature(tsaSig).withAdditionalSignature(ourSignature)
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
// We should run it through our full TransactionGroup of all transactions here.
logger.trace { "Built finished transaction, sending back to secondary!" }
send(TRADE_TOPIC, otherSide, buyerSessionID, fullySigned)
return Pair(wtx, ltx)
}
}
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
private class SellerTradeInfo(
class SellerTradeInfo(
val assetForSale: StateAndRef<OwnableState>,
val price: Amount,
val sellerOwnerKey: PublicKey,
val sessionID: Long
)
class SignaturesFromSeller(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey)
open class Seller(val otherSide: SingleMessageRecipient,
val timestampingAuthority: LegallyIdentifiableNode,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount,
val myKeyPair: KeyPair,
val buyerSessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
@Suspendable
override fun call(): Pair<WireTransaction, LedgerTransaction> {
val partialTX: SignedWireTransaction = receiveAndCheckProposedTransaction()
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
val ourSignature = signWithOurKey(partialTX)
val tsaSig = timestamp(partialTX)
val ledgerTX = sendSignatures(partialTX, ourSignature, tsaSig)
return Pair(partialTX.tx, ledgerTX)
}
@Suspendable
open fun receiveAndCheckProposedTransaction(): SignedWireTransaction {
val sessionID = random63BitValue()
// Make the first message we'll send to kick off the protocol.
val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID)
val maybePartialTX = sendAndReceive(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java)
val partialTX = maybePartialTX.validate {
it.verifySignatures()
logger.trace { "Received partially signed transaction" }
val wtx: WireTransaction = it.tx
requireThat {
"transaction sends us the right amount of cash" by (wtx.outputs.sumCashBy(myKeyPair.public) == price)
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
//
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
// we're reusing keys! So don't reuse keys!
// - This tx may not be valid according to the contracts of the input states, so we must resolve
// and fully audit the transaction chains to convince ourselves that it is actually valid.
// - This tx may include output states that impose odd conditions on the movement of the cash,
// once we implement state pairing.
//
// but the goal of this code is not to be fully secure, but rather, just to find good ways to
// express protocol state machines on top of the messaging layer.
}
}
return partialTX
}
open fun signWithOurKey(partialTX: SignedWireTransaction) = myKeyPair.signWithECDSA(partialTX.txBits)
@Suspendable
open fun timestamp(partialTX: SignedWireTransaction): DigitalSignature.LegallyIdentifiable {
return TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits)
}
@Suspendable
open fun sendSignatures(partialTX: SignedWireTransaction, ourSignature: DigitalSignature.WithKey,
tsaSig: DigitalSignature.LegallyIdentifiable): LedgerTransaction {
val fullySigned = partialTX + tsaSig + ourSignature
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
// TODO: We should run it through our full TransactionGroup of all transactions here.
logger.trace { "Built finished transaction, sending back to secondary!" }
send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(tsaSig, ourSignature))
return ltx
}
}
class UnacceptablePriceException(val givenPrice: Amount) : Exception()
class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() {
override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
}
// The buyer's side of the protocol. See note above Seller to learn about the caveats here.
class Buyer(val otherSide: SingleMessageRecipient,
val timestampingAuthority: Party,
val acceptablePrice: Amount,
val typeToBuy: Class<out OwnableState>,
val sessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
open class Buyer(val otherSide: SingleMessageRecipient,
val timestampingAuthority: Party,
val acceptablePrice: Amount,
val typeToBuy: Class<out OwnableState>,
val sessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
@Suspendable
override fun call(): Pair<WireTransaction, LedgerTransaction> {
val tradeRequest = receiveAndValidateTradeRequest()
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID)
logger.trace { "Got signatures from seller, verifying ... "}
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
logger.trace { "Fully signed transaction was valid. Trade complete! :-)" }
return Pair(fullySigned.tx, ltx)
}
@Suspendable
open fun receiveAndValidateTradeRequest(): SellerTradeInfo {
// Wait for a trade request to come in on our pre-provided session ID.
val tradeRequest = receive(TRADE_TOPIC, sessionID, SellerTradeInfo::class.java)
val maybeTradeRequest = receive(TRADE_TOPIC, sessionID, SellerTradeInfo::class.java)
// What is the seller trying to sell us?
val assetTypeName = tradeRequest.assetForSale.state.javaClass.name
logger.trace { "Got trade request for a $assetTypeName" }
val tradeRequest = maybeTradeRequest.validate {
// What is the seller trying to sell us?
val assetTypeName = it.assetForSale.state.javaClass.name
logger.trace { "Got trade request for a $assetTypeName" }
// Check the start message for acceptability.
check(tradeRequest.sessionID > 0)
if (tradeRequest.price > acceptablePrice)
throw UnacceptablePriceException(tradeRequest.price)
if (!typeToBuy.isInstance(tradeRequest.assetForSale.state))
throw AssetMismatchException(typeToBuy.name, assetTypeName)
// Check the start message for acceptability.
check(it.sessionID > 0)
if (it.price > acceptablePrice)
throw UnacceptablePriceException(it.price)
if (!typeToBuy.isInstance(it.assetForSale.state))
throw AssetMismatchException(typeToBuy.name, assetTypeName)
}
// TODO: Either look up the stateref here in our local db, or accept a long chain of states and
// validate them to audit the other side and ensure it actually owns the state we are being offered!
// For now, just assume validity!
return tradeRequest
}
// Generate the shared transaction that both sides will sign, using the data we have.
@Suspendable
open fun swapSignaturesWithSeller(stx: SignedWireTransaction, theirSessionID: Long): SignaturesFromSeller {
logger.trace { "Sending partially signed transaction to seller" }
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
return sendAndReceive(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx, SignaturesFromSeller::class.java).validate {}
}
open fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedWireTransaction {
// Now sign the transaction with whatever keys we need to move the cash.
for (k in cashSigningPubKeys) {
val priv = serviceHub.keyManagementService.toPrivate(k)
ptx.signWith(KeyPair(k, priv))
}
val stx = ptx.toSignedTransaction(checkSufficientSignatures = false)
stx.verifySignatures() // Verifies that we generated a signed transaction correctly.
// TODO: Could run verify() here to make sure the only signature missing is the sellers.
return stx
}
open fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
val ptx = TransactionBuilder()
// Add input and output states for the movement of cash, by using the Cash contract to generate the states.
val wallet = serviceHub.walletService.currentWallet
@ -178,33 +242,7 @@ object TwoPartyTradeProtocol {
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
// to have one.
ptx.setTime(Instant.now(), timestampingAuthority, 30.seconds)
// Now sign the transaction with whatever keys we need to move the cash.
for (k in cashSigningPubKeys) {
val priv = serviceHub.keyManagementService.toPrivate(k)
ptx.signWith(KeyPair(k, priv))
}
val stx = ptx.toSignedTransaction(checkSufficientSignatures = false)
stx.verifySignatures() // Verifies that we generated a signed transaction correctly.
// TODO: Could run verify() here to make sure the only signature missing is the sellers.
logger.trace { "Sending partially signed transaction to seller" }
// TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx.
// TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
val fullySigned = sendAndReceive(TRADE_TOPIC, otherSide, tradeRequest.sessionID,
sessionID, stx, SignedWireTransaction::class.java)
logger.trace { "Got fully signed transaction, verifying ... "}
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
logger.trace { "Fully signed transaction was valid. Trade complete! :-)" }
return Pair(fullySigned.tx, ltx)
return Pair(ptx, cashSigningPubKeys)
}
}
}

View File

@ -79,7 +79,7 @@ data class WireTransaction(val inputs: List<StateRef>,
data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, val sigs: List<DigitalSignature.WithKey>) {
init { check(sigs.isNotEmpty()) }
// Lazily calculated access to the deserialised/hashed transaction data.
/** Lazily calculated access to the deserialised/hashed transaction data. */
val tx: WireTransaction by lazy { txBits.deserialize() }
/** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */
@ -124,6 +124,9 @@ data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, v
/** Returns the same transaction but with an additional (unchecked) signature */
fun withAdditionalSignature(sig: DigitalSignature.WithKey) = copy(sigs = sigs + sig)
/** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */
operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig)
}
/** A mutable transaction that's in the process of being built, before all signatures are present. */

View File

@ -118,8 +118,7 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor)
}
/**
* Kicks off a brand new state machine of the given class. It will log with the named logger, and the
* [initialArgs] object will be passed to the call method of the [ProtocolStateMachine] object.
* Kicks off a brand new state machine of the given class. It will log with the named logger.
* The state machine will be persisted when it suspends, with automated restart if the StateMachineManager is
* restarted with checkpointed state machines in the storage service.
*/
@ -262,7 +261,7 @@ abstract class ProtocolStateMachine<R> : Fiber<R>("protocol", SameThreadFiberSch
}
@Suspendable @Suppress("UNCHECKED_CAST")
private fun <T : Any> suspendAndExpectReceive(with: FiberRequest): T {
private fun <T : Any> suspendAndExpectReceive(with: FiberRequest): UntrustworthyData<T> {
Fiber.parkAndSerialize { fiber, serializer ->
// We don't use the passed-in serializer here, because we need to use our own augmented Kryo.
val deserializer = Fiber.getFiberSerializer() as KryoSerializer
@ -275,18 +274,18 @@ abstract class ProtocolStateMachine<R> : Fiber<R>("protocol", SameThreadFiberSch
}
val tmp = resumeWithObject ?: throw IllegalStateException("Expected to receive something")
resumeWithObject = null
return tmp as T
return UntrustworthyData(tmp as T)
}
@Suspendable @Suppress("UNCHECKED_CAST")
fun <T : Any> sendAndReceive(topic: String, destination: MessageRecipients, sessionIDForSend: Long, sessionIDForReceive: Long,
obj: Any, recvType: Class<T>): T {
obj: Any, recvType: Class<T>): UntrustworthyData<T> {
val result = FiberRequest.ExpectingResponse(topic, destination, sessionIDForSend, sessionIDForReceive, obj, recvType)
return suspendAndExpectReceive(result)
}
@Suspendable
fun <T : Any> receive(topic: String, sessionIDForReceive: Long, recvType: Class<T>): T {
fun <T : Any> receive(topic: String, sessionIDForReceive: Long, recvType: Class<T>): UntrustworthyData<T> {
val result = FiberRequest.ExpectingResponse(topic, null, -1, sessionIDForReceive, null, recvType)
return suspendAndExpectReceive(result)
}
@ -298,6 +297,29 @@ abstract class ProtocolStateMachine<R> : Fiber<R>("protocol", SameThreadFiberSch
}
}
/**
* A small utility to approximate taint tracking: if a method gives you back one of these, it means the data came from
* a remote source that may be incentivised to pass us junk that violates basic assumptions and thus must be checked
* first. The wrapper helps you to avoid forgetting this vital step. Things you might want to check are:
*
* - Is this object the one you actually expected? Did the other side hand you back something technically valid but
* not what you asked for?
* - Is the object disobeying its own invariants?
* - Are any objects *reachable* from this object mismatched or not what you expected?
* - Is it suspiciously large or small?
*/
class UntrustworthyData<T>(private val fromUntrustedWorld: T) {
val data: T
@Deprecated("Accessing the untrustworthy data directly without validating it first is a bad idea")
get() = fromUntrustedWorld
@Suppress("DEPRECATION")
inline fun validate(validator: (T) -> Unit): T {
validator(data)
return data
}
}
// TODO: Clean this up
open class FiberRequest(val topic: String, val destination: MessageRecipients?,
val sessionIDForSend: Long, val sessionIDForReceive: Long, val obj: Any?) {

View File

@ -117,10 +117,13 @@ class TimestamperClient(private val psm: ProtocolStateMachine<*>, private val no
val sessionID = random63BitValue()
val replyTopic = "${TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC}.$sessionID"
val req = TimestampingMessages.Request(wtxBytes, psm.serviceHub.networkService.myAddress, replyTopic)
val signature = psm.sendAndReceive(TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC, node.address, 0,
val maybeSignature = psm.sendAndReceive(TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC, node.address, 0,
sessionID, req, DigitalSignature.LegallyIdentifiable::class.java)
// Check that the timestamping authority gave us back a valid signature and didn't break somehow
signature.verifyWithECDSA(wtxBytes)
val signature = maybeSignature.validate { it.verifyWithECDSA(wtxBytes) }
return signature
}
}

View File

@ -14,14 +14,12 @@ import contracts.CommercialPaper
import contracts.protocols.TwoPartyTradeProtocol
import core.*
import core.testutils.*
import core.utilities.BriefLogFormatter
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Formatter
import java.util.logging.Level
import java.util.logging.LogRecord
import java.util.logging.Logger
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@ -32,24 +30,21 @@ import kotlin.test.assertTrue
* We assume that Alice and Bob already found each other via some market, and have agreed the details already.
*/
class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
lateinit var backgroundThread: ExecutorService
@Before
fun initLogging() {
Logger.getLogger("").handlers[0].level = Level.ALL
Logger.getLogger("").handlers[0].formatter = object : Formatter() {
override fun format(record: LogRecord) = "${record.threadID} ${record.loggerName}: ${record.message}\n"
}
Logger.getLogger("com.r3cev.protocols.trade").level = Level.ALL
fun before() {
backgroundThread = Executors.newSingleThreadExecutor()
BriefLogFormatter.initVerbose("platform.trade")
}
@After
fun stopLogging() {
Logger.getLogger("com.r3cev.protocols.trade").level = Level.INFO
fun after() {
backgroundThread.shutdown()
}
@Test
fun cashForCP() {
val backgroundThread = Executors.newSingleThreadExecutor()
transactionGroupFor<ContractState> {
// Bob (Buyer) has some cash, Alice (Seller) has some commercial paper she wants to sell to Bob.
roots {
@ -96,7 +91,6 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
txns.add(aliceResult.get().second)
verify()
}
backgroundThread.shutdown()
}
@Test