diff --git a/docs/source/protocol-state-machines.rst b/docs/source/protocol-state-machines.rst index be3cf0c2ef..1cd7724e50 100644 --- a/docs/source/protocol-state-machines.rst +++ b/docs/source/protocol-state-machines.rst @@ -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, price: Amount, - myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture> { + myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture { 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, - sessionID: Long): ListenableFuture> { + sessionID: Long): ListenableFuture { 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, val price: Amount, val myKeyPair: KeyPair, - val buyerSessionID: Long) : ProtocolStateMachine>() { + val buyerSessionID: Long) : ProtocolStateMachine() { @Suspendable - override fun call(): Pair { + override fun call(): SignedTransaction { TODO() } } @@ -156,9 +156,9 @@ each side. val timestampingAuthority: Party, val acceptablePrice: Amount, val typeToBuy: Class, - val sessionID: Long) : ProtocolStateMachine>() { + val sessionID: Long) : ProtocolStateMachine() { @Suspendable - override fun call(): Pair { + 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``, which is just a marker class that reminds +It returns a simple wrapper class, ``UntrustworthyData``, 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 { + 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, ptx: TransactionBuilder): SignedWireTransaction { + open fun signWithOurKeys(cashSigningPubKeys: List, 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) diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 7ca1b6dbe2..a9231cd3f2 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -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. diff --git a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt index 9843d9b97b..2d8a65fd06 100644 --- a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt +++ b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt @@ -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, price: Amount, - myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture> { + myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture { 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, - sessionID: Long): ListenableFuture> { + sessionID: Long): ListenableFuture { 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, val price: Amount, val myKeyPair: KeyPair, - val buyerSessionID: Long) : ProtocolStateMachine>() { + val buyerSessionID: Long) : ProtocolStateMachine() { @Suspendable - override fun call(): Pair { - 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, - val sessionID: Long) : ProtocolStateMachine>() { + val sessionID: Long) : ProtocolStateMachine() { @Suspendable - override fun call(): Pair { + 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, ptx: TransactionBuilder): SignedWireTransaction { + open fun signWithOurKeys(cashSigningPubKeys: List, 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) diff --git a/src/main/kotlin/core/Transactions.kt b/src/main/kotlin/core/Transactions.kt index 4c4fc96170..5b74607d16 100644 --- a/src/main/kotlin/core/Transactions.kt +++ b/src/main/kotlin/core/Transactions.kt @@ -57,12 +57,30 @@ import java.util.* data class WireTransaction(val inputs: List, val outputs: List, val commands: List) { - 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? = null + val serialized: SerializedBytes get() = cachedBits ?: serialize().apply { cachedBits = this } + val id: SecureHash get() = serialized.hash + companion object { + fun deserialize(bits: SerializedBytes): 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): SignedTransaction { + return SignedTransaction(serialized, withSigs) } override fun toString(): String { @@ -76,11 +94,11 @@ data class WireTransaction(val inputs: List, } /** Container for a [WireTransaction] and attached signatures. */ -data class SignedWireTransaction(val txBits: SerializedBytes, val sigs: List) { +data class SignedTransaction(val txBits: SerializedBytes, val sigs: List) { 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, 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 = 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 = 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 = 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 = arrayListOf */ data class LedgerTransaction( /** The input states which will be consumed/invalidated by the execution of this transaction. */ - val inputs: List, + val inputs: List, /** The states that will be generated by the execution of this transaction. */ - val outputs: List, + val outputs: List, /** Arbitrary data passed to the program of each input state. */ - val commands: List>, - /** The hash of the original serialised SignedTransaction */ - val hash: SecureHash + val commands: List>, + /** The hash of the original serialised WireTransaction */ + val hash: SecureHash ) { @Suppress("UNCHECKED_CAST") fun outRef(index: Int) = StateAndRef(outputs[index] as T, StateRef(hash, index)) - fun outRef(state: T): StateAndRef { - 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 = emptyList(), allowUnusedKeys: Boolean = false): SignedTransaction { + val allPubKeys = commands.flatMap { it.signers }.toSet() + val wtx = toWireTransaction() + val bits = wtx.serialize() + val sigs = ArrayList() + 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) } } \ No newline at end of file diff --git a/src/main/kotlin/core/node/TraderDemo.kt b/src/main/kotlin/core/node/TraderDemo.kt index 01c84d6fcd..5380c9c7d7 100644 --- a/src/main/kotlin/core/node/TraderDemo.kt +++ b/src/main/kotlin/core/node/TraderDemo.kt @@ -110,7 +110,7 @@ fun main(args: Array) { 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) { println() println("Final transaction is") println() - println(Emoji.renderIfSupported(it.first)) + println(Emoji.renderIfSupported(it)) println() node.stop() } diff --git a/src/main/kotlin/core/serialization/Kryo.kt b/src/main/kotlin/core/serialization/Kryo.kt index 0cbb28054d..b18a34c4bb 100644 --- a/src/main/kotlin/core/serialization/Kryo.kt +++ b/src/main/kotlin/core/serialization/Kryo.kt @@ -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 serialize properly so we can use "by lazy" in serialized object. } } diff --git a/src/test/kotlin/core/TransactionGroupTests.kt b/src/test/kotlin/core/TransactionGroupTests.kt index cfbfeb8502..a43fdb452d 100644 --- a/src/test/kotlin/core/TransactionGroupTests.kt +++ b/src/test/kotlin/core/TransactionGroupTests.kt @@ -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 = 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) + } } \ No newline at end of file diff --git a/src/main/kotlin/core/messaging/InMemoryNetwork.kt b/src/test/kotlin/core/messaging/InMemoryNetwork.kt similarity index 97% rename from src/main/kotlin/core/messaging/InMemoryNetwork.kt rename to src/test/kotlin/core/messaging/InMemoryNetwork.kt index 80180384ec..090562ee04 100644 --- a/src/main/kotlin/core/messaging/InMemoryNetwork.kt +++ b/src/test/kotlin/core/messaging/InMemoryNetwork.kt @@ -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) } diff --git a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt index 88f3b96220..f1cdd3562f 100644 --- a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt +++ b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt @@ -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()) diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index 1dccf00ef3..faa369a276 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -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 = 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(private val stateType: Class) { - open inner class LedgerTransactionDSL : AbstractTransactionForTest() { + open inner class WireTransactionDSL : AbstractTransactionForTest() { private val inStates = ArrayList() 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(private val stateType: Class) { fun 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() + private val rootTxns = ArrayList() private val labelToRefs = HashMap() private val labelToOutputs = HashMap() private val outputsToLabels = HashMap() @@ -255,42 +247,45 @@ class TransactionGroupDSL(private val stateType: Class) { 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() - private val txnToLabelMap = HashMap() + val txns = ArrayList() + private val txnToLabelMap = HashMap() - 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.() -> 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(private val stateType: Class) { 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(private val stateType: Class) { @Suppress("CAST_NEVER_SUCCEEDS") GraphVisualiser(this as TransactionGroupDSL).display() } + + fun signAll(): List { + return txns.map { wtx -> + val allPubKeys = wtx.commands.flatMap { it.pubkeys }.toSet() + val bits = wtx.serialize() + require(bits == wtx.serialized) + val sigs = ArrayList() + for (key in ALL_TEST_KEYS) { + if (allPubKeys.contains(key.public)) + sigs += key.signWithECDSA(bits) + } + wtx.toSignedTransaction(sigs) + } + } } inline fun transactionGroupFor(body: TransactionGroupDSL.() -> Unit) = TransactionGroupDSL(T::class.java).apply { this.body() }