mirror of
https://github.com/corda/corda.git
synced 2025-01-31 00:24:59 +00:00
Merged in tests-with-signing-improvements (pull request #19)
Improve unit tests to support signatures, refactor transaction classes a bit
This commit is contained in:
commit
8bd54dfc74
@ -86,10 +86,10 @@ Our protocol has two parties (B and S for buyer and seller) and will proceed as
|
||||
|
||||
1. S sends a ``StateAndRef`` pointing to the state they want to sell to B, along with info about the price they require
|
||||
B to pay.
|
||||
2. B sends to S a ``SignedWireTransaction`` that includes the state as input, B's cash as input, the state with the new
|
||||
2. B sends to S a ``SignedTransaction`` that includes the state as input, B's cash as input, the state with the new
|
||||
owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because
|
||||
it lacks a signature from S authorising movement of the asset.
|
||||
3. S signs it and hands the now finalised ``SignedWireTransaction`` back to B.
|
||||
3. S signs it and hands the now finalised ``SignedTransaction`` back to B.
|
||||
|
||||
You can find the implementation of this protocol in the file ``contracts/protocols/TwoPartyTradeProtocol.kt``.
|
||||
|
||||
@ -111,7 +111,7 @@ each side.
|
||||
|
||||
fun runSeller(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
|
||||
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
|
||||
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<Pair<WireTransaction, LedgerTransaction>> {
|
||||
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val seller = Seller(otherSide, timestampingAuthority, assetToSell, price, myKeyPair, buyerSessionID)
|
||||
smm.add("$TRADE_TOPIC.seller", seller)
|
||||
return seller.resultFuture
|
||||
@ -119,7 +119,7 @@ each side.
|
||||
|
||||
fun runBuyer(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
|
||||
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
|
||||
sessionID: Long): ListenableFuture<Pair<WireTransaction, LedgerTransaction>> {
|
||||
sessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val buyer = Buyer(otherSide, timestampingAuthority.identity, acceptablePrice, typeToBuy, sessionID)
|
||||
smm.add("$TRADE_TOPIC.buyer", buyer)
|
||||
return buyer.resultFuture
|
||||
@ -140,9 +140,9 @@ each side.
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
val myKeyPair: KeyPair,
|
||||
val buyerSessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
|
||||
val buyerSessionID: Long) : ProtocolStateMachine<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): Pair<WireTransaction, LedgerTransaction> {
|
||||
override fun call(): SignedTransaction {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
@ -156,9 +156,9 @@ each side.
|
||||
val timestampingAuthority: Party,
|
||||
val acceptablePrice: Amount,
|
||||
val typeToBuy: Class<out OwnableState>,
|
||||
val sessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
|
||||
val sessionID: Long) : ProtocolStateMachine<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): Pair<WireTransaction, LedgerTransaction> {
|
||||
override fun call(): SignedTransaction {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
@ -245,15 +245,15 @@ Let's implement the ``Seller.call`` method. This will be invoked by the platform
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val partialTX: SignedWireTransaction = receiveAndCheckProposedTransaction()
|
||||
val partialTX: SignedTransaction = 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)
|
||||
val stx: SignedTransaction = sendSignatures(partialTX, ourSignature, tsaSig)
|
||||
|
||||
return Pair(partialTX.tx, ledgerTX)
|
||||
return stx
|
||||
|
||||
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
|
||||
@ -267,13 +267,13 @@ Let's fill out the ``receiveAndCheckProposedTransaction()`` method.
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
@Suspendable
|
||||
open fun receiveAndCheckProposedTransaction(): SignedWireTransaction {
|
||||
open fun receiveAndCheckProposedTransaction(): SignedTransaction {
|
||||
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 maybePartialTX = sendAndReceive(TRADE_TOPIC, buyerSessionID, sessionID, hello, SignedTransaction::class.java)
|
||||
val partialTX = maybePartialTX.validate {
|
||||
it.verifySignatures()
|
||||
logger.trace { "Received partially signed transaction" }
|
||||
@ -305,7 +305,7 @@ 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
|
||||
It returns a simple wrapper class, ``UntrustworthyData<SignedTransaction>``, 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.
|
||||
@ -329,25 +329,25 @@ Here's the rest of the code:
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
open fun signWithOurKey(partialTX: SignedWireTransaction) = myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
open fun signWithOurKey(partialTX: SignedTransaction) = myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
|
||||
@Suspendable
|
||||
open fun timestamp(partialTX: SignedWireTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
open fun timestamp(partialTX: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
return TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
open fun sendSignatures(partialTX: SignedWireTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
tsaSig: DigitalSignature.LegallyIdentifiable): LedgerTransaction {
|
||||
open fun sendSignatures(partialTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
tsaSig: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||
val fullySigned = partialTX + tsaSig + ourSignature
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
|
||||
fullySigned.verify()
|
||||
|
||||
// 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
|
||||
return fullySigned
|
||||
}
|
||||
|
||||
It's should be all pretty straightforward: here, ``txBits`` is the raw byte array representing the transaction.
|
||||
@ -372,7 +372,7 @@ OK, let's do the same for the buyer side:
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Pair<WireTransaction, LedgerTransaction> {
|
||||
override fun call(): SignedTransaction {
|
||||
val tradeRequest = receiveAndValidateTradeRequest()
|
||||
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
|
||||
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
|
||||
@ -380,10 +380,10 @@ OK, let's do the same for the buyer side:
|
||||
|
||||
logger.trace { "Got signatures from seller, verifying ... "}
|
||||
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
|
||||
fullySigned.verify()
|
||||
|
||||
logger.trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
||||
return Pair(fullySigned.tx, ltx)
|
||||
return fullySigned
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -411,7 +411,7 @@ OK, let's do the same for the buyer side:
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
open fun swapSignaturesWithSeller(stx: SignedWireTransaction, theirSessionID: Long): SignaturesFromSeller {
|
||||
open fun swapSignaturesWithSeller(stx: SignedTransaction, 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.
|
||||
@ -419,7 +419,7 @@ OK, let's do the same for the buyer side:
|
||||
return sendAndReceive(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx, SignaturesFromSeller::class.java).validate {}
|
||||
}
|
||||
|
||||
open fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedWireTransaction {
|
||||
open fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
||||
// Now sign the transaction with whatever keys we need to move the cash.
|
||||
for (k in cashSigningPubKeys) {
|
||||
val priv = serviceHub.keyManagementService.toPrivate(k)
|
||||
|
@ -744,7 +744,7 @@ A ``TransactionBuilder`` is not by itself ready to be used anywhere, so first, w
|
||||
is recognised by the network. The most important next step is for the participating entities to sign it using the
|
||||
``signWith()`` method. This takes a keypair, serialises the transaction, signs the serialised form and then stores the
|
||||
signature inside the ``TransactionBuilder``. Once all parties have signed, you can call ``TransactionBuilder.toSignedTransaction()``
|
||||
to get a ``SignedWireTransaction`` object. This is an immutable form of the transaction that's ready for *timestamping*,
|
||||
to get a ``SignedTransaction`` object. This is an immutable form of the transaction that's ready for *timestamping*,
|
||||
which can be done using a ``TimestamperClient``. To learn more about that, please refer to the
|
||||
:doc:`protocol-state-machines` document.
|
||||
|
||||
|
@ -31,7 +31,7 @@ import java.time.Instant
|
||||
*
|
||||
* 1. S sends the [StateAndRef] pointing to what they want to sell to B, along with info about the price they require
|
||||
* B to pay. For example this has probably been agreed on an exchange.
|
||||
* 2. B sends to S a [SignedWireTransaction] that includes the state as input, B's cash as input, the state with the new
|
||||
* 2. B sends to S a [SignedTransaction] that includes the state as input, B's cash as input, the state with the new
|
||||
* owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because
|
||||
* it lacks a signature from S authorising movement of the asset.
|
||||
* 3. S signs it and hands the now finalised SignedWireTransaction back to B.
|
||||
@ -53,7 +53,7 @@ object TwoPartyTradeProtocol {
|
||||
|
||||
fun runSeller(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
|
||||
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
|
||||
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<Pair<WireTransaction, LedgerTransaction>> {
|
||||
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val seller = Seller(otherSide, timestampingAuthority, assetToSell, price, myKeyPair, buyerSessionID)
|
||||
smm.add("$TRADE_TOPIC.seller", seller)
|
||||
return seller.resultFuture
|
||||
@ -61,7 +61,7 @@ object TwoPartyTradeProtocol {
|
||||
|
||||
fun runBuyer(smm: StateMachineManager, timestampingAuthority: LegallyIdentifiableNode,
|
||||
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
|
||||
sessionID: Long): ListenableFuture<Pair<WireTransaction, LedgerTransaction>> {
|
||||
sessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val buyer = Buyer(otherSide, timestampingAuthority.identity, acceptablePrice, typeToBuy, sessionID)
|
||||
smm.add("$TRADE_TOPIC.buyer", buyer)
|
||||
return buyer.resultFuture
|
||||
@ -82,28 +82,28 @@ object TwoPartyTradeProtocol {
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount,
|
||||
val myKeyPair: KeyPair,
|
||||
val buyerSessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
|
||||
val buyerSessionID: Long) : ProtocolStateMachine<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): Pair<WireTransaction, LedgerTransaction> {
|
||||
val partialTX: SignedWireTransaction = receiveAndCheckProposedTransaction()
|
||||
override fun call(): SignedTransaction {
|
||||
val partialTX: SignedTransaction = 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)
|
||||
val signedTransaction = sendSignatures(partialTX, ourSignature, tsaSig)
|
||||
|
||||
return Pair(partialTX.tx, ledgerTX)
|
||||
return signedTransaction
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
open fun receiveAndCheckProposedTransaction(): SignedWireTransaction {
|
||||
open fun receiveAndCheckProposedTransaction(): SignedTransaction {
|
||||
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 maybePartialTX = sendAndReceive(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello, SignedTransaction::class.java)
|
||||
val partialTX = maybePartialTX.validate {
|
||||
it.verifySignatures()
|
||||
logger.trace { "Received partially signed transaction" }
|
||||
@ -127,25 +127,24 @@ object TwoPartyTradeProtocol {
|
||||
return partialTX
|
||||
}
|
||||
|
||||
open fun signWithOurKey(partialTX: SignedWireTransaction) = myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
open fun signWithOurKey(partialTX: SignedTransaction) = myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
|
||||
@Suspendable
|
||||
open fun timestamp(partialTX: SignedWireTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
open fun timestamp(partialTX: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
return TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
open fun sendSignatures(partialTX: SignedWireTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
tsaSig: DigitalSignature.LegallyIdentifiable): LedgerTransaction {
|
||||
open fun sendSignatures(partialTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
tsaSig: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||
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
|
||||
return fullySigned
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,9 +157,9 @@ object TwoPartyTradeProtocol {
|
||||
val timestampingAuthority: Party,
|
||||
val acceptablePrice: Amount,
|
||||
val typeToBuy: Class<out OwnableState>,
|
||||
val sessionID: Long) : ProtocolStateMachine<Pair<WireTransaction, LedgerTransaction>>() {
|
||||
val sessionID: Long) : ProtocolStateMachine<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): Pair<WireTransaction, LedgerTransaction> {
|
||||
override fun call(): SignedTransaction {
|
||||
val tradeRequest = receiveAndValidateTradeRequest()
|
||||
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
|
||||
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
|
||||
@ -168,10 +167,10 @@ object TwoPartyTradeProtocol {
|
||||
|
||||
logger.trace { "Got signatures from seller, verifying ... "}
|
||||
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
|
||||
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
|
||||
fullySigned.verify()
|
||||
|
||||
logger.trace { "Fully signed transaction was valid. Trade complete! :-)" }
|
||||
return Pair(fullySigned.tx, ltx)
|
||||
logger.trace { "Signatures received are valid. Trade complete! :-)" }
|
||||
return fullySigned
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -199,7 +198,7 @@ object TwoPartyTradeProtocol {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
open fun swapSignaturesWithSeller(stx: SignedWireTransaction, theirSessionID: Long): SignaturesFromSeller {
|
||||
open fun swapSignaturesWithSeller(stx: SignedTransaction, 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.
|
||||
@ -207,7 +206,7 @@ object TwoPartyTradeProtocol {
|
||||
return sendAndReceive(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx, SignaturesFromSeller::class.java).validate {}
|
||||
}
|
||||
|
||||
open fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedWireTransaction {
|
||||
open fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
||||
// Now sign the transaction with whatever keys we need to move the cash.
|
||||
for (k in cashSigningPubKeys) {
|
||||
val priv = serviceHub.keyManagementService.toPrivate(k)
|
||||
|
@ -57,12 +57,30 @@ import java.util.*
|
||||
data class WireTransaction(val inputs: List<StateRef>,
|
||||
val outputs: List<ContractState>,
|
||||
val commands: List<Command>) {
|
||||
fun toLedgerTransaction(identityService: IdentityService, originalHash: SecureHash): LedgerTransaction {
|
||||
|
||||
// Cache the serialised form of the transaction and its hash to give us fast access to it.
|
||||
@Volatile @Transient private var cachedBits: SerializedBytes<WireTransaction>? = null
|
||||
val serialized: SerializedBytes<WireTransaction> get() = cachedBits ?: serialize().apply { cachedBits = this }
|
||||
val id: SecureHash get() = serialized.hash
|
||||
companion object {
|
||||
fun deserialize(bits: SerializedBytes<WireTransaction>): WireTransaction {
|
||||
val wtx = bits.deserialize()
|
||||
wtx.cachedBits = bits
|
||||
return wtx
|
||||
}
|
||||
}
|
||||
|
||||
fun toLedgerTransaction(identityService: IdentityService): LedgerTransaction {
|
||||
val authenticatedArgs = commands.map {
|
||||
val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) }
|
||||
AuthenticatedObject(it.pubkeys, institutions, it.data)
|
||||
}
|
||||
return LedgerTransaction(inputs, outputs, authenticatedArgs, originalHash)
|
||||
return LedgerTransaction(inputs, outputs, authenticatedArgs, id)
|
||||
}
|
||||
|
||||
/** Serialises and returns this transaction as a [SignedTransaction] with no signatures attached. */
|
||||
fun toSignedTransaction(withSigs: List<DigitalSignature.WithKey>): SignedTransaction {
|
||||
return SignedTransaction(serialized, withSigs)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
@ -76,11 +94,11 @@ data class WireTransaction(val inputs: List<StateRef>,
|
||||
}
|
||||
|
||||
/** Container for a [WireTransaction] and attached signatures. */
|
||||
data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, val sigs: List<DigitalSignature.WithKey>) {
|
||||
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>, val sigs: List<DigitalSignature.WithKey>) {
|
||||
init { check(sigs.isNotEmpty()) }
|
||||
|
||||
/** Lazily calculated access to the deserialised/hashed transaction data. */
|
||||
val tx: WireTransaction by lazy { txBits.deserialize() }
|
||||
val tx: WireTransaction by lazy { WireTransaction.deserialize(txBits) }
|
||||
|
||||
/** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */
|
||||
val id: SecureHash get() = txBits.hash
|
||||
@ -119,7 +137,7 @@ data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, v
|
||||
*/
|
||||
fun verifyToLedgerTransaction(identityService: IdentityService): LedgerTransaction {
|
||||
verify()
|
||||
return tx.toLedgerTransaction(identityService, id)
|
||||
return tx.toLedgerTransaction(identityService)
|
||||
}
|
||||
|
||||
/** Returns the same transaction but with an additional (unchecked) signature */
|
||||
@ -155,7 +173,7 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
|
||||
}
|
||||
|
||||
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
|
||||
public fun withItems(vararg items: Any): TransactionBuilder {
|
||||
fun withItems(vararg items: Any): TransactionBuilder {
|
||||
for (t in items) {
|
||||
when (t) {
|
||||
is StateRef -> inputs.add(t)
|
||||
@ -219,7 +237,7 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
|
||||
|
||||
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(outputs), ArrayList(commands))
|
||||
|
||||
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedWireTransaction {
|
||||
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction {
|
||||
if (checkSufficientSignatures) {
|
||||
val gotKeys = currentSigs.map { it.by }.toSet()
|
||||
for (command in commands) {
|
||||
@ -227,7 +245,7 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
|
||||
throw IllegalStateException("Missing signatures on the transaction for a ${command.data.javaClass.canonicalName} command")
|
||||
}
|
||||
}
|
||||
return SignedWireTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
|
||||
return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
|
||||
}
|
||||
|
||||
fun addInputState(ref: StateRef) {
|
||||
@ -263,21 +281,40 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
|
||||
*/
|
||||
data class LedgerTransaction(
|
||||
/** The input states which will be consumed/invalidated by the execution of this transaction. */
|
||||
val inputs: List<StateRef>,
|
||||
val inputs: List<StateRef>,
|
||||
/** The states that will be generated by the execution of this transaction. */
|
||||
val outputs: List<ContractState>,
|
||||
val outputs: List<ContractState>,
|
||||
/** Arbitrary data passed to the program of each input state. */
|
||||
val commands: List<AuthenticatedObject<CommandData>>,
|
||||
/** The hash of the original serialised SignedTransaction */
|
||||
val hash: SecureHash
|
||||
val commands: List<AuthenticatedObject<CommandData>>,
|
||||
/** The hash of the original serialised WireTransaction */
|
||||
val hash: SecureHash
|
||||
) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as T, StateRef(hash, index))
|
||||
|
||||
fun <T : ContractState> outRef(state: T): StateAndRef<T> {
|
||||
val i = outputs.indexOf(state)
|
||||
if (i == -1)
|
||||
throw IllegalArgumentException("State not found in this transaction")
|
||||
return outRef(i)
|
||||
fun toWireTransaction(): WireTransaction {
|
||||
val wtx = WireTransaction(inputs, outputs, commands.map { Command(it.value, it.signers) })
|
||||
check(wtx.serialize().hash == hash)
|
||||
return wtx
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this transaction to [SignedTransaction] form, optionally using the provided keys to sign. There is
|
||||
* no requirement that [andSignWithKeys] include all required keys.
|
||||
*
|
||||
* @throws IllegalArgumentException if a key is provided that isn't listed in any command and [allowUnusedKeys]
|
||||
* is false.
|
||||
*/
|
||||
fun toSignedTransaction(andSignWithKeys: List<KeyPair> = emptyList(), allowUnusedKeys: Boolean = false): SignedTransaction {
|
||||
val allPubKeys = commands.flatMap { it.signers }.toSet()
|
||||
val wtx = toWireTransaction()
|
||||
val bits = wtx.serialize()
|
||||
val sigs = ArrayList<DigitalSignature.WithKey>()
|
||||
for (key in andSignWithKeys) {
|
||||
if (!allPubKeys.contains(key.public) && !allowUnusedKeys)
|
||||
throw IllegalArgumentException("Key provided that is not listed by any command")
|
||||
sigs += key.signWithECDSA(bits)
|
||||
}
|
||||
return wtx.toSignedTransaction(sigs)
|
||||
}
|
||||
}
|
@ -110,7 +110,7 @@ fun main(args: Array<String>) {
|
||||
println()
|
||||
println("Purchase complete - we are a happy customer! Final transaction is:")
|
||||
println()
|
||||
println(Emoji.renderIfSupported(it.first))
|
||||
println(Emoji.renderIfSupported(it))
|
||||
println()
|
||||
println("Waiting for another seller to connect. Or press Ctrl-C to shut me down.")
|
||||
}
|
||||
@ -145,7 +145,7 @@ fun main(args: Array<String>) {
|
||||
println()
|
||||
println("Final transaction is")
|
||||
println()
|
||||
println(Emoji.renderIfSupported(it.first))
|
||||
println(Emoji.renderIfSupported(it))
|
||||
println()
|
||||
node.stop()
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.serializers.JavaSerializer
|
||||
import core.SignedWireTransaction
|
||||
import core.SignedTransaction
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.generateKeyPair
|
||||
import core.crypto.sha256
|
||||
@ -206,12 +206,14 @@ fun createKryo(k: Kryo = Kryo()): Kryo {
|
||||
// Some classes have to be handled with the ImmutableClassSerializer because they need to have their
|
||||
// constructors be invoked (typically for lazy members).
|
||||
val immutables = listOf(
|
||||
SignedWireTransaction::class,
|
||||
SignedTransaction::class,
|
||||
SerializedBytes::class
|
||||
)
|
||||
|
||||
immutables.forEach {
|
||||
register(it.java, ImmutableClassSerializer(it))
|
||||
}
|
||||
|
||||
// TODO: See if we can make Lazy<T> serialize properly so we can use "by lazy" in serialized object.
|
||||
}
|
||||
}
|
||||
|
@ -74,8 +74,8 @@ class TransactionGroupTests {
|
||||
val e = assertFailsWith(TransactionConflictException::class) {
|
||||
verify()
|
||||
}
|
||||
assertEquals(StateRef(t.hash, 0), e.conflictRef)
|
||||
assertEquals(setOf(conflict1, conflict2), setOf(e.tx1, e.tx2))
|
||||
assertEquals(StateRef(t.id, 0), e.conflictRef)
|
||||
assertEquals(setOf(conflict1, conflict2), setOf(e.tx1.toWireTransaction(), e.tx2.toWireTransaction()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,9 +97,11 @@ class TransactionGroupTests {
|
||||
// We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that
|
||||
// points nowhere.
|
||||
val ref = StateRef(SecureHash.randomSHA256(), 0)
|
||||
tg.txns.add(LedgerTransaction(
|
||||
listOf(ref), listOf(A_THOUSAND_POUNDS), listOf(AuthenticatedObject(listOf(BOB), emptyList(), Cash.Commands.Move())), SecureHash.randomSHA256())
|
||||
)
|
||||
tg.txns += TransactionBuilder().apply {
|
||||
addInputState(ref)
|
||||
addOutputState(A_THOUSAND_POUNDS)
|
||||
addCommand(Cash.Commands.Move(), BOB)
|
||||
}.toWireTransaction()
|
||||
|
||||
val e = assertFailsWith(TransactionResolutionException::class) {
|
||||
tg.verify()
|
||||
@ -127,4 +129,30 @@ class TransactionGroupTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signGroup() {
|
||||
val signedTxns: List<SignedTransaction> = transactionGroup {
|
||||
transaction {
|
||||
output("£1000") { A_THOUSAND_POUNDS }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("£1000")
|
||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("alice's £1000")
|
||||
arg(ALICE) { Cash.Commands.Move() }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) }
|
||||
}
|
||||
}.signAll()
|
||||
|
||||
// Now go through the conversion -> verification path with them.
|
||||
val ltxns = signedTxns.map { it.verifyToLedgerTransaction(MockIdentityService) }.toSet()
|
||||
TransactionGroup(ltxns, emptySet()).verify(MockContractFactory)
|
||||
}
|
||||
}
|
@ -11,8 +11,7 @@ package core.messaging
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import core.Party
|
||||
import core.crypto.generateKeyPair
|
||||
import core.DummyTimestampingAuthority
|
||||
import core.crypto.sha256
|
||||
import core.node.TimestamperNodeService
|
||||
import core.utilities.loggerFor
|
||||
@ -126,10 +125,8 @@ public class InMemoryNetwork {
|
||||
check(timestampingAdvert == null)
|
||||
val (handle, builder) = createNode(manuallyPumped)
|
||||
val node = builder.start().get()
|
||||
val key = generateKeyPair()
|
||||
val identity = Party("Unit test timestamping authority", key.public)
|
||||
TimestamperNodeService(node, identity, key)
|
||||
timestampingAdvert = LegallyIdentifiableNode(handle, identity)
|
||||
TimestamperNodeService(node, DummyTimestampingAuthority.identity, DummyTimestampingAuthority.key)
|
||||
timestampingAdvert = LegallyIdentifiableNode(handle, DummyTimestampingAuthority.identity)
|
||||
return Pair(timestampingAdvert!!, node)
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
|
||||
assertEquals(aliceResult.get(), bobResult.get())
|
||||
|
||||
txns.add(aliceResult.get().second)
|
||||
txns.add(aliceResult.get().tx)
|
||||
verify()
|
||||
}
|
||||
}
|
||||
@ -178,8 +178,8 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() {
|
||||
assertTrue(bobsNode.pump(false))
|
||||
|
||||
// Bob is now finished and has the same transaction as Alice.
|
||||
val tx = bobFuture.get()
|
||||
txns.add(tx.second)
|
||||
val stx = bobFuture.get()
|
||||
txns.add(stx.tx)
|
||||
verify()
|
||||
|
||||
assertTrue(smm.stateMachines.isEmpty())
|
||||
|
@ -12,10 +12,8 @@ package core.testutils
|
||||
|
||||
import contracts.*
|
||||
import core.*
|
||||
import core.crypto.DummyPublicKey
|
||||
import core.crypto.NullPublicKey
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.generateKeyPair
|
||||
import core.crypto.*
|
||||
import core.serialization.serialize
|
||||
import core.visualiser.GraphVisualiser
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
@ -43,6 +41,8 @@ val BOB = BOB_KEY.public
|
||||
val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY)
|
||||
val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY)
|
||||
|
||||
val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY)
|
||||
|
||||
val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Party> = mapOf(
|
||||
MEGA_CORP_PUBKEY to MEGA_CORP,
|
||||
MINI_CORP_PUBKEY to MINI_CORP
|
||||
@ -205,22 +205,14 @@ open class TransactionForTest : AbstractTransactionForTest() {
|
||||
fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().apply { body() }
|
||||
|
||||
class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
open inner class LedgerTransactionDSL : AbstractTransactionForTest() {
|
||||
open inner class WireTransactionDSL : AbstractTransactionForTest() {
|
||||
private val inStates = ArrayList<StateRef>()
|
||||
|
||||
fun input(label: String) {
|
||||
inStates.add(label.outputRef)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts to a [LedgerTransaction] with the test institution map, and just assigns a random hash
|
||||
* (i.e. pretend it was signed)
|
||||
*/
|
||||
fun toLedgerTransaction(): LedgerTransaction {
|
||||
val wtx = WireTransaction(inStates, outStates.map { it.state }, commands)
|
||||
return wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256())
|
||||
}
|
||||
fun toWireTransaction() = WireTransaction(inStates, outStates.map { it.state }, commands)
|
||||
}
|
||||
|
||||
val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found")
|
||||
@ -228,23 +220,23 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
|
||||
fun <C : ContractState> lookup(label: String) = StateAndRef(label.output as C, label.outputRef)
|
||||
|
||||
private inner class InternalLedgerTransactionDSL : LedgerTransactionDSL() {
|
||||
fun finaliseAndInsertLabels(): LedgerTransaction {
|
||||
val ltx = toLedgerTransaction()
|
||||
private inner class InternalWireTransactionDSL : WireTransactionDSL() {
|
||||
fun finaliseAndInsertLabels(): WireTransaction {
|
||||
val wtx = toWireTransaction()
|
||||
for ((index, labelledState) in outStates.withIndex()) {
|
||||
if (labelledState.label != null) {
|
||||
labelToRefs[labelledState.label] = StateRef(ltx.hash, index)
|
||||
labelToRefs[labelledState.label] = StateRef(wtx.id, index)
|
||||
if (stateType.isInstance(labelledState.state)) {
|
||||
labelToOutputs[labelledState.label] = labelledState.state as T
|
||||
}
|
||||
outputsToLabels[labelledState.state] = labelledState.label
|
||||
}
|
||||
}
|
||||
return ltx
|
||||
return wtx
|
||||
}
|
||||
}
|
||||
|
||||
private val rootTxns = ArrayList<LedgerTransaction>()
|
||||
private val rootTxns = ArrayList<WireTransaction>()
|
||||
private val labelToRefs = HashMap<String, StateRef>()
|
||||
private val labelToOutputs = HashMap<String, T>()
|
||||
private val outputsToLabels = HashMap<ContractState, String>()
|
||||
@ -255,42 +247,45 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
fun transaction(vararg outputStates: LabeledOutput) {
|
||||
val outs = outputStates.map { it.state }
|
||||
val wtx = WireTransaction(emptyList(), outs, emptyList())
|
||||
val ltx = wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256())
|
||||
for ((index, state) in outputStates.withIndex()) {
|
||||
val label = state.label!!
|
||||
labelToRefs[label] = StateRef(ltx.hash, index)
|
||||
labelToRefs[label] = StateRef(wtx.id, index)
|
||||
outputsToLabels[state.state] = label
|
||||
labelToOutputs[label] = state.state as T
|
||||
}
|
||||
rootTxns.add(ltx)
|
||||
rootTxns.add(wtx)
|
||||
}
|
||||
|
||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||
fun roots(body: Roots.() -> Unit) {}
|
||||
@Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR)
|
||||
fun transaction(body: LedgerTransactionDSL.() -> Unit) {}
|
||||
fun transaction(body: WireTransactionDSL.() -> Unit) {}
|
||||
}
|
||||
fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
|
||||
|
||||
val txns = ArrayList<LedgerTransaction>()
|
||||
private val txnToLabelMap = HashMap<LedgerTransaction, String>()
|
||||
val txns = ArrayList<WireTransaction>()
|
||||
private val txnToLabelMap = HashMap<SecureHash, String>()
|
||||
|
||||
fun transaction(label: String? = null, body: LedgerTransactionDSL.() -> Unit): LedgerTransaction {
|
||||
val forTest = InternalLedgerTransactionDSL()
|
||||
fun transaction(label: String? = null, body: WireTransactionDSL.() -> Unit): WireTransaction {
|
||||
val forTest = InternalWireTransactionDSL()
|
||||
forTest.body()
|
||||
val ltx = forTest.finaliseAndInsertLabels()
|
||||
txns.add(ltx)
|
||||
val wtx = forTest.finaliseAndInsertLabels()
|
||||
txns.add(wtx)
|
||||
if (label != null)
|
||||
txnToLabelMap[ltx] = label
|
||||
return ltx
|
||||
txnToLabelMap[wtx.id] = label
|
||||
return wtx
|
||||
}
|
||||
|
||||
fun labelForTransaction(ltx: LedgerTransaction): String? = txnToLabelMap[ltx]
|
||||
fun labelForTransaction(tx: WireTransaction): String? = txnToLabelMap[tx.id]
|
||||
fun labelForTransaction(tx: LedgerTransaction): String? = txnToLabelMap[tx.hash]
|
||||
|
||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||
fun transactionGroup(body: TransactionGroupDSL<T>.() -> Unit) {}
|
||||
|
||||
fun toTransactionGroup() = TransactionGroup(txns.map { it }.toSet(), rootTxns.toSet())
|
||||
fun toTransactionGroup() = TransactionGroup(
|
||||
txns.map { it.toLedgerTransaction(MockIdentityService) }.toSet(),
|
||||
rootTxns.map { it.toLedgerTransaction(MockIdentityService) }.toSet()
|
||||
)
|
||||
|
||||
class Failed(val index: Int, cause: Throwable) : Exception("Transaction $index didn't verify", cause)
|
||||
|
||||
@ -300,8 +295,8 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
group.verify(MockContractFactory)
|
||||
} catch (e: TransactionVerificationException) {
|
||||
// Let the developer know the index of the transaction that failed.
|
||||
val ltx: LedgerTransaction = txns.find { it.hash == e.tx.origHash }!!
|
||||
throw Failed(txns.indexOf(ltx) + 1, e)
|
||||
val wtx: WireTransaction = txns.find { it.id == e.tx.origHash }!!
|
||||
throw Failed(txns.indexOf(wtx) + 1, e)
|
||||
}
|
||||
}
|
||||
|
||||
@ -319,6 +314,20 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||
GraphVisualiser(this as TransactionGroupDSL<ContractState>).display()
|
||||
}
|
||||
|
||||
fun signAll(): List<SignedTransaction> {
|
||||
return txns.map { wtx ->
|
||||
val allPubKeys = wtx.commands.flatMap { it.pubkeys }.toSet()
|
||||
val bits = wtx.serialize()
|
||||
require(bits == wtx.serialized)
|
||||
val sigs = ArrayList<DigitalSignature.WithKey>()
|
||||
for (key in ALL_TEST_KEYS) {
|
||||
if (allPubKeys.contains(key.public))
|
||||
sigs += key.signWithECDSA(bits)
|
||||
}
|
||||
wtx.toSignedTransaction(sigs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> transactionGroupFor(body: TransactionGroupDSL<T>.() -> Unit) = TransactionGroupDSL<T>(T::class.java).apply { this.body() }
|
||||
|
Loading…
x
Reference in New Issue
Block a user