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:
Mike Hearn 2016-02-12 20:01:55 +01:00
commit 8bd54dfc74
10 changed files with 193 additions and 121 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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.
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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())

View File

@ -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() }