diff --git a/.gitignore b/.gitignore index ca5a51b4d5..d97c406cb6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,8 @@ TODO /build/ /docs/build/doctrees -alpha -beta +buyer +seller ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio diff --git a/docs/source/protocol-state-machines.rst b/docs/source/protocol-state-machines.rst index 57c54148aa..be3cf0c2ef 100644 --- a/docs/source/protocol-state-machines.rst +++ b/docs/source/protocol-state-machines.rst @@ -125,6 +125,16 @@ each side. return buyer.resultFuture } + // This object is serialised to the network and is the first protocol message the seller sends to the buyer. + class SellerTradeInfo( + val assetForSale: StateAndRef, + val price: Amount, + val sellerOwnerKey: PublicKey, + val sessionID: Long + ) + + class SignaturesFromSeller(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey) + class Seller(val otherSide: SingleMessageRecipient, val timestampingAuthority: LegallyIdentifiableNode, val assetToSell: StateAndRef, @@ -137,21 +147,11 @@ each side. } } - // This object is serialised to the network and is the first protocol message the seller sends to the buyer. - private class SellerTradeInfo( - val assetForSale: StateAndRef, - val price: Amount, - val sellerOwnerKey: PublicKey, - val sessionID: Long - ) - - class UnacceptablePriceException(val givenPrice: Amount) : Exception() class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() { override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" } - // The buyer's side of the protocol. See note above Seller to learn about the caveats here. class Buyer(val otherSide: SingleMessageRecipient, val timestampingAuthority: Party, val acceptablePrice: Amount, @@ -167,7 +167,7 @@ each side. Let's unpack what this code does: - It defines a several classes nested inside the main ``TwoPartyTradeProtocol`` singleton, and a couple of methods, one - to run the buyer side of the protocol and one to run the seller side. + to run the buyer side of the protocol and one to run the seller side. Some of the classes are simply protocol messages. - It defines the "trade topic", which is just a string that namespaces this protocol. The prefix "platform." is reserved by the DLG, but you can define your own protocols using standard Java-style reverse DNS notation. - The ``runBuyer`` and ``runSeller`` methods take a number of parameters that specialise the protocol for this run, @@ -205,7 +205,8 @@ to either runBuyer or runSeller, depending on who we are, and then call ``.get() block the calling thread until the protocol has finished. Or we could register a callback on the returned future that will be invoked when it's done, where we could e.g. update a user interface. -Finally, we define a couple of exceptions, and a class that will be used as a protocol message called ``SellerTradeInfo``. +Finally, we define a couple of exceptions, and two classes that will be used as a protocol message called +``SellerTradeInfo`` and ``SignaturesFromSeller``. Suspendable methods ------------------- @@ -244,13 +245,57 @@ Let's implement the ``Seller.call`` method. This will be invoked by the platform .. sourcecode:: kotlin - val sessionID = random63BitValue() + val partialTX: SignedWireTransaction = receiveAndCheckProposedTransaction() - // Make the first message we'll send to kick off the protocol. - val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID) + // These two steps could be done in parallel, in theory. Our framework doesn't support that yet though. + val ourSignature = signWithOurKey(partialTX) + val tsaSig = timestamp(partialTX) - val partialTX = sendAndReceive(TRADE_TOPIC, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java) - logger().trace { "Received partially signed transaction" } + val ledgerTX = sendSignatures(partialTX, ourSignature, tsaSig) + + return Pair(partialTX.tx, ledgerTX) + +Here we see the outline of the procedure. We receive a proposed trade transaction from the buyer and check that it's +valid. Then we sign with our own key, request a timestamping authority to assert with another signature that the +timestamp in the transaction (if any) is valid, and finally we send back both our signature and the TSA's signature. +Finally, we hand back to the code that invoked the protocol the finished transaction in a couple of different forms. + +Let's fill out the ``receiveAndCheckProposedTransaction()`` method. + +.. container:: codeset + + .. sourcecode:: kotlin + + @Suspendable + open fun receiveAndCheckProposedTransaction(): SignedWireTransaction { + val sessionID = random63BitValue() + + // Make the first message we'll send to kick off the protocol. + val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID) + + val maybePartialTX = sendAndReceive(TRADE_TOPIC, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java) + val partialTX = maybePartialTX.validate { + it.verifySignatures() + logger.trace { "Received partially signed transaction" } + val wtx: WireTransaction = it.tx + + requireThat { + "transaction sends us the right amount of cash" by (wtx.outputs.sumCashBy(myKeyPair.public) == price) + // There are all sorts of funny games a malicious secondary might play here, we should fix them: + // + // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if + // we're reusing keys! So don't reuse keys! + // - This tx may not be valid according to the contracts of the input states, so we must resolve + // and fully audit the transaction chains to convince ourselves that it is actually valid. + // - This tx may include output states that impose odd conditions on the movement of the cash, + // once we implement state pairing. + // + // but the goal of this code is not to be fully secure, but rather, just to find good ways to + // express protocol state machines on top of the messaging layer. + } + } + return partialTX + } That's pretty straightforward. We generate a session ID to identify what's happening on the seller side, fill out the initial protocol message, and then call ``sendAndReceive``. This function takes a few arguments: @@ -260,6 +305,11 @@ the initial protocol message, and then call ``sendAndReceive``. This function ta - The thing to send. It'll be serialised and sent automatically. - Finally a type argument, which is the kind of object we're expecting to receive from the other side. +It returns a simple wrapper class, ``UntrustworthyData``, which is just a marker class that reminds +us that the data came from a potentially malicious external source and may have been tampered with or be unexpected in +other ways. It doesn't add any functionality, but acts as a reminder to "scrub" the data before use. Here, our scrubbing +simply involves checking the signatures on it. Then we could go ahead and do some more involved checks. + Once sendAndReceive is called, the call method will be suspended into a continuation. When it gets back we'll do a log message. The buyer is supposed to send us a transaction with all the right inputs/outputs/commands in return, with their cash put into the transaction and their signature on it authorising the movement of the cash. @@ -273,51 +323,45 @@ cash put into the transaction and their signature on it authorising the movement doing things like creating threads from inside these calls would be a bad idea. They should only contain business logic. -OK, let's keep going: +Here's the rest of the code: .. container:: codeset .. sourcecode:: kotlin - partialTX.verifySignatures() - val wtx = partialTX.txBits.deserialize() + open fun signWithOurKey(partialTX: SignedWireTransaction) = myKeyPair.signWithECDSA(partialTX.txBits) - requireThat { - "transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price) - // There are all sorts of funny games a malicious secondary might play here, we should fix them: - // - // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if - // we're reusing keys! So don't reuse keys! - // - This tx may not be valid according to the contracts of the input states, so we must resolve - // and fully audit the transaction chains to convince ourselves that it is actually valid. - // - This tx may include output states that impose odd conditions on the movement of the cash, - // once we implement state pairing. + @Suspendable + open fun timestamp(partialTX: SignedWireTransaction): DigitalSignature.LegallyIdentifiable { + return TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits) } - val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits.bits) - val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature) - fullySigned.verify() - val timestamped: TimestampedWireTransaction = fullySigned.toTimestampedTransaction(serviceHub.timestampingService) - logger().trace { "Built finished transaction, sending back to secondary!" } + @Suspendable + open fun sendSignatures(partialTX: SignedWireTransaction, ourSignature: DigitalSignature.WithKey, + tsaSig: DigitalSignature.LegallyIdentifiable): LedgerTransaction { + val fullySigned = partialTX + tsaSig + ourSignature + val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) - send(TRADE_TOPIC, sessionID, timestamped) + // TODO: We should run it through our full TransactionGroup of all transactions here. - return Pair(timestamped, timestamped.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)) + logger.trace { "Built finished transaction, sending back to secondary!" } -Here, we see some assertions and signature checking to satisfy ourselves that we're not about to sign something -incorrect. Once we're happy, we calculate a signature over the transaction to authorise the movement of the asset -we are selling, and then we verify things to make sure it's all OK. Finally, we request timestamping of the -transaction, in case the contracts governing the asset we're selling require it, and send the now finalised and -validated transaction back to the buyer. + send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(tsaSig, ourSignature)) + return ltx + } + +It's should be all pretty straightforward: here, ``txBits`` is the raw byte array representing the transaction. + +In ``sendSignatures``, we take the two signatures we calculated, then add them to the partial transaction we were sent +and verify that the signatures all make sense. This should never fail: it's just a sanity check. Finally, we wrap the +two signatures in a simple wrapper message class and send it back. The send won't block waiting for an acknowledgement, +but the underlying message queue software will retry delivery if the other side has gone away temporarily. .. warning:: This code is **not secure**. Other than not checking for all possible invalid constructions, if the seller stops before sending the finalised transaction to the buyer, the seller is left with a valid transaction but the buyer isn't, so they can't spend the asset they just purchased! This sort of thing will be fixed in a future version of the code. -Finally, the call function returns with the result of the protocol: in our case, the final transaction in two different -forms. - Implementing the buyer ---------------------- @@ -328,41 +372,54 @@ OK, let's do the same for the buyer side: .. sourcecode:: kotlin @Suspendable - override fun call(): Pair { + override fun call(): Pair { + val tradeRequest = receiveAndValidateTradeRequest() + val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest) + val stx = signWithOurKeys(cashSigningPubKeys, ptx) + val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID) + + logger.trace { "Got signatures from seller, verifying ... "} + val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig + val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) + + logger.trace { "Fully signed transaction was valid. Trade complete! :-)" } + return Pair(fullySigned.tx, ltx) + } + + @Suspendable + open fun receiveAndValidateTradeRequest(): SellerTradeInfo { // Wait for a trade request to come in on our pre-provided session ID. - val tradeRequest = receive(TRADE_TOPIC, args.sessionID, SellerTradeInfo::class.java) + val maybeTradeRequest = receive(TRADE_TOPIC, sessionID, SellerTradeInfo::class.java) - // What is the seller trying to sell us? - val assetTypeName = tradeRequest.assetForSale.state.javaClass.name - logger().trace { "Got trade request for a $assetTypeName" } + val tradeRequest = maybeTradeRequest.validate { + // What is the seller trying to sell us? + val assetTypeName = it.assetForSale.state.javaClass.name + logger.trace { "Got trade request for a $assetTypeName" } - // Check the start message for acceptability. - check(tradeRequest.sessionID > 0) - if (tradeRequest.price > acceptablePrice) - throw UnacceptablePriceException(tradeRequest.price) - if (!typeToBuy.isInstance(tradeRequest.assetForSale.state)) - throw AssetMismatchException(typeToBuy.name, assetTypeName) + // Check the start message for acceptability. + check(it.sessionID > 0) + if (it.price > acceptablePrice) + throw UnacceptablePriceException(it.price) + if (!typeToBuy.isInstance(it.assetForSale.state)) + throw AssetMismatchException(typeToBuy.name, assetTypeName) + } - // TODO: Either look up the stateref here in our local db, or accept a long chain - // of states and validate them to audit the other side and ensure it actually owns - // the state we are being offered! For now, just assume validity! + // TODO: Either look up the stateref here in our local db, or accept a long chain of states and + // validate them to audit the other side and ensure it actually owns the state we are being offered! + // For now, just assume validity! + return tradeRequest + } - // Generate the shared transaction that both sides will sign, using the data we have. - val ptx = TransactionBuilder() - // Add input and output states for the movement of cash, by using the Cash contract - // to generate the states. - val wallet = serviceHub.walletService.currentWallet - val cashStates = wallet.statesOfType() - val cashSigningPubKeys = Cash().craftSpend(ptx, tradeRequest.price, - tradeRequest.sellerOwnerKey, cashStates) - // Add inputs/outputs/a command for the movement of the asset. - ptx.addInputState(tradeRequest.assetForSale.ref) - // Just pick some new public key for now. - val freshKey = serviceHub.keyManagementService.freshKey() - val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public) - ptx.addOutputState(state) - ptx.addArg(WireCommand(command, tradeRequest.assetForSale.state.owner)) + @Suspendable + open fun swapSignaturesWithSeller(stx: SignedWireTransaction, theirSessionID: Long): SignaturesFromSeller { + logger.trace { "Sending partially signed transaction to seller" } + // TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx. + + return sendAndReceive(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx, SignaturesFromSeller::class.java).validate {} + } + + open fun signWithOurKeys(cashSigningPubKeys: List, ptx: TransactionBuilder): SignedWireTransaction { // Now sign the transaction with whatever keys we need to move the cash. for (k in cashSigningPubKeys) { val priv = serviceHub.keyManagementService.toPrivate(k) @@ -374,36 +431,43 @@ OK, let's do the same for the buyer side: // TODO: Could run verify() here to make sure the only signature missing is the sellers. - logger().trace { "Sending partially signed transaction to seller" } + return stx + } - // TODO: Protect against the buyer terminating here and leaving us in the lurch without - // the final tx. - // TODO: Protect against a malicious buyer sending us back a different transaction to - // the one we built. - val fullySigned = sendAndReceive(TRADE_TOPIC, tradeRequest.sessionID, sessionID, stx, - TimestampedWireTransaction::class.java) + open fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> { + val ptx = TransactionBuilder() + // Add input and output states for the movement of cash, by using the Cash contract to generate the states. + val wallet = serviceHub.walletService.currentWallet + val cashStates = wallet.statesOfType() + val cashSigningPubKeys = Cash().generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates) + // Add inputs/outputs/a command for the movement of the asset. + ptx.addInputState(tradeRequest.assetForSale.ref) + // Just pick some new public key for now. This won't be linked with our identity in any way, which is what + // we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to + // reveal who the owner actually is. The key management service is expected to derive a unique key from some + // initial seed in order to provide privacy protection. + val freshKey = serviceHub.keyManagementService.freshKey() + val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public) + ptx.addOutputState(state) + ptx.addCommand(command, tradeRequest.assetForSale.state.owner) - logger().trace { "Got fully signed transaction, verifying ... "} - - val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, - serviceHub.identityService) - - logger().trace { "Fully signed transaction was valid. Trade complete! :-)" } - - return Pair(fullySigned, ltx) + // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt + // to have one. + ptx.setTime(Instant.now(), timestampingAuthority, 30.seconds) + return Pair(ptx, cashSigningPubKeys) } This code is longer but still fairly straightforward. Here are some things to pay attention to: 1. We do some sanity checking on the received message to ensure we're being offered what we expected to be offered. -2. We create a cash spend in the normal way, by using ``Cash().craftSpend``. See the contracts tutorial if this isn't +2. We create a cash spend in the normal way, by using ``Cash().generateSpend``. See the contracts tutorial if this isn't clear. 3. We access the *service hub* when we need it to access things that are transient and may change or be recreated whilst a protocol is suspended, things like the wallet or the timestamping service. Remember that a protocol may be suspended when it waits to receive a message across node or computer restarts, so objects representing a service or data which may frequently change should be accessed 'just in time'. -4. Finally, we send the unfinsished, invalid transaction to the seller so they can sign it. They are expected to send - back to us a ``TimestampedWireTransaction``, which once we verify it, should be the final outcome of the trade. +4. Finally, we send the unfinished, invalid transaction to the seller so they can sign it. They are expected to send + back to us a ``SignaturesFromSeller``, which once we verify it, should be the final outcome of the trade. As you can see, the protocol logic is straightforward and does not contain any callbacks or network glue code, despite the fact that it takes minimal resources and can survive node restarts. diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index dd5faf2b73..7ca1b6dbe2 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -636,22 +636,22 @@ again to ensure the third transaction fails with a message that contains "must h the exact message). -Adding a crafting API to your contract +Adding a generation API to your contract -------------------------------------- Contract classes **must** provide a verify function, but they may optionally also provide helper functions to simplify -their usage. A simple class of functions most contracts provide are *crafting functions*, which either generate or +their usage. A simple class of functions most contracts provide are *generation functions*, which either create or modify a transaction to perform certain actions (an action is normally mappable 1:1 to a command, but doesn't have to be so). -Crafting may involve complex logic. For example, the cash contract has a ``craftSpend`` method that is given a set of +Generation may involve complex logic. For example, the cash contract has a ``generateSpend`` method that is given a set of cash states and chooses a way to combine them together to satisfy the amount of money that is being sent. In the immutable-state model that we are using ledger entries (states) can only be created and deleted, but never modified. Therefore to send $1200 when we have only $900 and $500 requires combining both states together, and then creating two new output states of $1200 and $200 back to ourselves. This latter state is called the *change* and is a concept that should be familiar to anyone who has worked with Bitcoin. -As another example, we can imagine code that implements a netting algorithm may craft complex transactions that must +As another example, we can imagine code that implements a netting algorithm may generate complex transactions that must be signed by many people. Whilst such code might be too big for a single utility method (it'd probably be sized more like a module), the basic concept is the same: preparation of a transaction using complex logic. @@ -662,7 +662,7 @@ a method to wrap up the issuance process: .. sourcecode:: kotlin - fun craftIssue(issuance: InstitutionReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder { + fun generateIssue(issuance: InstitutionReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder { val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate) return TransactionBuilder(state, WireCommand(Commands.Issue, issuance.party.owningKey)) } @@ -673,7 +673,7 @@ returns a ``TransactionBuilder``. A ``TransactionBuilder`` is one of the few mut It allows you to add inputs, outputs and commands to it and is designed to be passed around, potentially between multiple contracts. -.. note:: Crafting methods should ideally be written to compose with each other, that is, they should take a +.. note:: Generation methods should ideally be written to compose with each other, that is, they should take a ``TransactionBuilder`` as an argument instead of returning one, unless you are sure it doesn't make sense to combine this type of transaction with others. In this case, issuing CP at the same time as doing other things would just introduce complexity that isn't likely to be worth it, so we return a fresh object each time: instead, @@ -697,7 +697,7 @@ What about moving the paper, i.e. reassigning ownership to someone else? .. sourcecode:: kotlin - fun craftMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { + fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { tx.addInputState(paper.ref) tx.addOutputState(paper.state.copy(owner = newOwner)) tx.addArg(WireCommand(Commands.Move, paper.state.owner)) @@ -705,7 +705,7 @@ What about moving the paper, i.e. reassigning ownership to someone else? Here, the method takes a pre-existing ``TransactionBuilder`` and adds to it. This is correct because typically you will want to combine a sale of CP atomically with the movement of some other asset, such as cash. So both -craft methods should operate on the same transaction. You can see an example of this being done in the unit tests +generate methods should operate on the same transaction. You can see an example of this being done in the unit tests for the commercial paper contract. The paper is given to us as a ``StateAndRef`` object. This is exactly what it sounds like: @@ -719,9 +719,9 @@ Finally, we can do redemption. .. sourcecode:: kotlin @Throws(InsufficientBalanceException::class) - fun craftRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { // Add the cash movement using the states in our wallet. - Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet) + Cash().generateSpend(tx, paper.state.faceValue, paper.state.owner, wallet) tx.addInputState(paper.ref) tx.addArg(WireCommand(CommercialPaper.Commands.Redeem, paper.state.owner)) } @@ -744,10 +744,9 @@ 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*. - -.. note:: Timestamping and passing around of partial transactions for group signing is not yet fully implemented. - This tutorial will be updated once it is. +to get a ``SignedWireTransaction`` 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. You can see how transactions flow through the different stages of construction by examining the commercial paper unit tests. diff --git a/src/main/java/contracts/JavaCommercialPaper.java b/src/main/java/contracts/JavaCommercialPaper.java index 3c67277552..a2b69e02e3 100644 --- a/src/main/java/contracts/JavaCommercialPaper.java +++ b/src/main/java/contracts/JavaCommercialPaper.java @@ -10,6 +10,7 @@ package contracts; import core.*; import core.TransactionForVerification.*; +import core.crypto.*; import org.jetbrains.annotations.*; import java.security.*; @@ -26,7 +27,7 @@ import static kotlin.collections.CollectionsKt.*; * */ public class JavaCommercialPaper implements Contract { - public static SecureHash JCP_PROGRAM_ID = SecureHash.Companion.sha256("java commercial paper (this should be a bytecode hash)"); + public static SecureHash JCP_PROGRAM_ID = SecureHash.sha256("java commercial paper (this should be a bytecode hash)"); public static class State implements ContractState, ICommercialPaperState { private PartyReference issuance; @@ -217,7 +218,7 @@ public class JavaCommercialPaper implements Contract { @Override public SecureHash getLegalContractReference() { // TODO: Should return hash of the contract's contents, not its URI - return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); + return SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); } public TransactionBuilder generateIssue(@NotNull PartyReference issuance, @NotNull Amount faceValue, @Nullable Instant maturityDate) { @@ -226,7 +227,7 @@ public class JavaCommercialPaper implements Contract { } public void generateRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { - new Cash().craftSpend(tx, paper.getState().getFaceValue(), paper.getState().getOwner(), wallet, null); + new Cash().generateSpend(tx, paper.getState().getFaceValue(), paper.getState().getOwner(), wallet, null); tx.addInputState(paper.getRef()); tx.addCommand(new Command( new Commands.Redeem(), paper.getState().getOwner())); } diff --git a/src/main/java/core/crypto/Base58.java b/src/main/java/core/crypto/Base58.java index a943d2500d..c7c7a3ca48 100644 --- a/src/main/java/core/crypto/Base58.java +++ b/src/main/java/core/crypto/Base58.java @@ -8,8 +8,6 @@ package core.crypto; -import core.*; - import java.math.*; import java.util.*; @@ -145,7 +143,7 @@ public class Base58 { throw new AddressFormatException("Input too short"); byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4); byte[] checksum = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length); - byte[] actualChecksum = Arrays.copyOfRange(SecureHash.Companion.sha256Twice(data).getBits(), 0, 4); + byte[] actualChecksum = Arrays.copyOfRange(SecureHash.sha256Twice(data).getBits(), 0, 4); if (!Arrays.equals(checksum, actualChecksum)) throw new AddressFormatException("Checksum does not validate"); return data; diff --git a/src/main/kotlin/core/Crypto.kt b/src/main/java/core/crypto/CryptoUtilities.kt similarity index 86% rename from src/main/kotlin/core/Crypto.kt rename to src/main/java/core/crypto/CryptoUtilities.kt index 57900d756e..2ecd0d35fc 100644 --- a/src/main/kotlin/core/Crypto.kt +++ b/src/main/java/core/crypto/CryptoUtilities.kt @@ -6,17 +6,17 @@ * All other rights reserved. */ -package core +package core.crypto import com.google.common.io.BaseEncoding -import core.crypto.Base58 +import core.Party import core.serialization.OpaqueBytes import java.math.BigInteger import java.security.* import java.security.interfaces.ECPublicKey // "sealed" here means there can't be any subclasses other than the ones defined here. -sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) { +sealed class SecureHash private constructor(bits: ByteArray) : OpaqueBytes(bits) { class SHA256(bits: ByteArray) : SecureHash(bits) { init { require(bits.size == 32) } override val signatureAlgorithmName: String get() = "SHA256withECDSA" @@ -28,18 +28,19 @@ sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) { // Like static methods in Java, except the 'companion' is a singleton that can have state. companion object { + @JvmStatic fun parse(str: String) = BaseEncoding.base16().decode(str.toLowerCase()).let { when (it.size) { - 32 -> SecureHash.SHA256(it) + 32 -> SHA256(it) else -> throw IllegalArgumentException("Provided string is not 32 bytes in base 16 (hex): $str") } } - fun sha256(bits: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bits)) - fun sha256Twice(bits: ByteArray) = sha256(sha256(bits).bits) - fun sha256(str: String) = sha256(str.toByteArray()) + @JvmStatic fun sha256(bits: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bits)) + @JvmStatic fun sha256Twice(bits: ByteArray) = sha256(sha256(bits).bits) + @JvmStatic fun sha256(str: String) = sha256(str.toByteArray()) - fun randomSHA256() = sha256(SecureRandom.getInstanceStrong().generateSeed(32)) + @JvmStatic fun randomSHA256() = sha256(SecureRandom.getInstanceStrong().generateSeed(32)) } abstract val signatureAlgorithmName: String @@ -122,4 +123,7 @@ fun PublicKey.toStringShort(): String { // Allow Kotlin destructuring: val (private, public) = keypair operator fun KeyPair.component1() = this.private -operator fun KeyPair.component2() = this.public \ No newline at end of file +operator fun KeyPair.component2() = this.public + +/** A simple wrapper that will make it easier to swap out the EC algorithm we use in future */ +fun generateKeyPair() = KeyPairGenerator.getInstance("EC").genKeyPair() \ No newline at end of file diff --git a/src/main/kotlin/contracts/Cash.kt b/src/main/kotlin/contracts/Cash.kt index 977ef609c5..fc75f2a904 100644 --- a/src/main/kotlin/contracts/Cash.kt +++ b/src/main/kotlin/contracts/Cash.kt @@ -9,6 +9,8 @@ package contracts import core.* +import core.crypto.SecureHash +import core.crypto.toStringShort import core.utilities.Emoji import java.security.PublicKey import java.security.SecureRandom @@ -152,7 +154,7 @@ class Cash : Contract { /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ - fun craftIssue(tx: TransactionBuilder, amount: Amount, at: PartyReference, owner: PublicKey) { + fun generateIssue(tx: TransactionBuilder, amount: Amount, at: PartyReference, owner: PublicKey) { check(tx.inputStates().isEmpty()) check(tx.outputStates().sumCashOrNull() == null) tx.addOutputState(Cash.State(at, amount, owner)) @@ -168,8 +170,8 @@ class Cash : Contract { * about which type of cash claims they are willing to accept. */ @Throws(InsufficientBalanceException::class) - fun craftSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, - cashStates: List>, onlyFromParties: Set? = null): List { + fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, + cashStates: List>, onlyFromParties: Set? = null): List { // Discussion // // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. diff --git a/src/main/kotlin/contracts/CommercialPaper.kt b/src/main/kotlin/contracts/CommercialPaper.kt index 16344b95b2..91645c2a7d 100644 --- a/src/main/kotlin/contracts/CommercialPaper.kt +++ b/src/main/kotlin/contracts/CommercialPaper.kt @@ -9,6 +9,9 @@ package contracts import core.* +import core.crypto.NullPublicKey +import core.crypto.SecureHash +import core.crypto.toStringShort import core.utilities.Emoji import java.security.PublicKey import java.time.Instant @@ -125,7 +128,7 @@ class CommercialPaper : Contract { * an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction * at the moment: this restriction is not fundamental and may be lifted later. */ - fun craftIssue(issuance: PartyReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder { + fun generateIssue(issuance: PartyReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder { val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate) return TransactionBuilder().withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) } @@ -133,7 +136,7 @@ class CommercialPaper : Contract { /** * Updates the given partial transaction with an input/output/command to reassign ownership of the paper. */ - fun craftMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { + fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { tx.addInputState(paper.ref) tx.addOutputState(paper.state.copy(owner = newOwner)) tx.addCommand(Commands.Move(), paper.state.owner) @@ -147,9 +150,9 @@ class CommercialPaper : Contract { * @throws InsufficientBalanceException if the wallet doesn't contain enough money to pay the redeemer */ @Throws(InsufficientBalanceException::class) - fun craftRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { // Add the cash movement using the states in our wallet. - Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet) + Cash().generateSpend(tx, paper.state.faceValue, paper.state.owner, wallet) tx.addInputState(paper.ref) tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner) } diff --git a/src/main/kotlin/contracts/CrowdFund.kt b/src/main/kotlin/contracts/CrowdFund.kt index 6b98fd92ec..5294e049ae 100644 --- a/src/main/kotlin/contracts/CrowdFund.kt +++ b/src/main/kotlin/contracts/CrowdFund.kt @@ -9,6 +9,7 @@ package contracts import core.* +import core.crypto.SecureHash import java.security.PublicKey import java.time.Instant import java.util.* @@ -142,7 +143,7 @@ class CrowdFund : Contract { * Returns a transaction that registers a crowd-funding campaing, owned by the issuing institution's key. Does not update * an existing transaction because it's not possible to register multiple campaigns in a single transaction */ - fun craftRegister(owner: PartyReference, fundingTarget: Amount, fundingName: String, closingTime: Instant): TransactionBuilder { + fun generateRegister(owner: PartyReference, fundingTarget: Amount, fundingName: String, closingTime: Instant): TransactionBuilder { val campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime) val state = State(campaign) return TransactionBuilder().withItems(state, Command(Commands.Register(), owner.party.owningKey)) @@ -151,7 +152,7 @@ class CrowdFund : Contract { /** * Updates the given partial transaction with an input/output/command to fund the opportunity. */ - fun craftPledge(tx: TransactionBuilder, campaign: StateAndRef, subscriber: PublicKey) { + fun generatePledge(tx: TransactionBuilder, campaign: StateAndRef, subscriber: PublicKey) { tx.addInputState(campaign.ref) tx.addOutputState(campaign.state.copy( pledges = campaign.state.pledges + CrowdFund.Pledge(subscriber, 1000.DOLLARS) @@ -159,14 +160,14 @@ class CrowdFund : Contract { tx.addCommand(Commands.Pledge(), subscriber) } - fun craftClose(tx: TransactionBuilder, campaign: StateAndRef, wallet: List>) { + fun generateClose(tx: TransactionBuilder, campaign: StateAndRef, wallet: List>) { tx.addInputState(campaign.ref) tx.addOutputState(campaign.state.copy(closed = true)) tx.addCommand(Commands.Close(), campaign.state.campaign.owner) // If campaign target has not been met, compose cash returns if (campaign.state.pledgedAmount < campaign.state.campaign.target) { for (pledge in campaign.state.pledges) { - Cash().craftSpend(tx, pledge.amount, pledge.owner, wallet) + Cash().generateSpend(tx, pledge.amount, pledge.owner, wallet) } } } diff --git a/src/main/kotlin/contracts/DummyContract.kt b/src/main/kotlin/contracts/DummyContract.kt index dd3bc4528c..52956d71c5 100644 --- a/src/main/kotlin/contracts/DummyContract.kt +++ b/src/main/kotlin/contracts/DummyContract.kt @@ -10,14 +10,14 @@ package contracts import core.Contract import core.ContractState -import core.SecureHash import core.TransactionForVerification +import core.crypto.SecureHash // The dummy contract doesn't do anything useful. It exists for testing purposes. val DUMMY_PROGRAM_ID = SecureHash.sha256("dummy") -object DummyContract : Contract { +class DummyContract : Contract { class State : ContractState { override val programRef: SecureHash = DUMMY_PROGRAM_ID } diff --git a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt index f438ef489f..9843d9b97b 100644 --- a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt +++ b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt @@ -13,12 +13,13 @@ import com.google.common.util.concurrent.ListenableFuture import contracts.Cash import contracts.sumCashBy import core.* +import core.crypto.DigitalSignature +import core.crypto.signWithECDSA import core.messaging.LegallyIdentifiableNode import core.messaging.ProtocolStateMachine import core.messaging.SingleMessageRecipient import core.messaging.StateMachineManager import core.node.TimestamperClient -import core.serialization.deserialize import core.utilities.trace import java.security.KeyPair import java.security.PublicKey @@ -66,103 +67,167 @@ object TwoPartyTradeProtocol { return buyer.resultFuture } - class Seller(val otherSide: SingleMessageRecipient, - val timestampingAuthority: LegallyIdentifiableNode, - val assetToSell: StateAndRef, - val price: Amount, - val myKeyPair: KeyPair, - val buyerSessionID: Long) : ProtocolStateMachine>() { - @Suspendable - override fun call(): Pair { - val sessionID = random63BitValue() - - // Make the first message we'll send to kick off the protocol. - val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID) - - val partialTX = sendAndReceive(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java) - logger.trace { "Received partially signed transaction" } - - partialTX.verifySignatures() - val wtx: WireTransaction = partialTX.txBits.deserialize() - - requireThat { - "transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(myKeyPair.public) == price) - // There are all sorts of funny games a malicious secondary might play here, we should fix them: - // - // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if - // we're reusing keys! So don't reuse keys! - // - This tx may not be valid according to the contracts of the input states, so we must resolve - // and fully audit the transaction chains to convince ourselves that it is actually valid. - // - This tx may include output states that impose odd conditions on the movement of the cash, - // once we implement state pairing. - // - // but the goal of this code is not to be fully secure, but rather, just to find good ways to - // express protocol state machines on top of the messaging layer. - } - - // Sign with our key and get the timestamping authorities key as well. - // These two steps could be done in parallel, in theory. - val ourSignature = myKeyPair.signWithECDSA(partialTX.txBits) - val tsaSig = TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits) - val fullySigned = partialTX.withAdditionalSignature(tsaSig).withAdditionalSignature(ourSignature) - val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) - - // We should run it through our full TransactionGroup of all transactions here. - - logger.trace { "Built finished transaction, sending back to secondary!" } - - send(TRADE_TOPIC, otherSide, buyerSessionID, fullySigned) - - return Pair(wtx, ltx) - } - } - // This object is serialised to the network and is the first protocol message the seller sends to the buyer. - private class SellerTradeInfo( + class SellerTradeInfo( val assetForSale: StateAndRef, val price: Amount, val sellerOwnerKey: PublicKey, val sessionID: Long ) + class SignaturesFromSeller(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey) + + open class Seller(val otherSide: SingleMessageRecipient, + val timestampingAuthority: LegallyIdentifiableNode, + val assetToSell: StateAndRef, + val price: Amount, + val myKeyPair: KeyPair, + val buyerSessionID: Long) : ProtocolStateMachine>() { + @Suspendable + override fun call(): Pair { + val partialTX: SignedWireTransaction = receiveAndCheckProposedTransaction() + + // These two steps could be done in parallel, in theory. Our framework doesn't support that yet though. + val ourSignature = signWithOurKey(partialTX) + val tsaSig = timestamp(partialTX) + + val ledgerTX = sendSignatures(partialTX, ourSignature, tsaSig) + + return Pair(partialTX.tx, ledgerTX) + } + + @Suspendable + open fun receiveAndCheckProposedTransaction(): SignedWireTransaction { + val sessionID = random63BitValue() + + // Make the first message we'll send to kick off the protocol. + val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID) + + val maybePartialTX = sendAndReceive(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello, SignedWireTransaction::class.java) + val partialTX = maybePartialTX.validate { + it.verifySignatures() + logger.trace { "Received partially signed transaction" } + val wtx: WireTransaction = it.tx + + requireThat { + "transaction sends us the right amount of cash" by (wtx.outputs.sumCashBy(myKeyPair.public) == price) + // There are all sorts of funny games a malicious secondary might play here, we should fix them: + // + // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if + // we're reusing keys! So don't reuse keys! + // - This tx may not be valid according to the contracts of the input states, so we must resolve + // and fully audit the transaction chains to convince ourselves that it is actually valid. + // - This tx may include output states that impose odd conditions on the movement of the cash, + // once we implement state pairing. + // + // but the goal of this code is not to be fully secure, but rather, just to find good ways to + // express protocol state machines on top of the messaging layer. + } + } + return partialTX + } + + open fun signWithOurKey(partialTX: SignedWireTransaction) = myKeyPair.signWithECDSA(partialTX.txBits) + + @Suspendable + open fun timestamp(partialTX: SignedWireTransaction): DigitalSignature.LegallyIdentifiable { + return TimestamperClient(this, timestampingAuthority).timestamp(partialTX.txBits) + } + + @Suspendable + open fun sendSignatures(partialTX: SignedWireTransaction, ourSignature: DigitalSignature.WithKey, + tsaSig: DigitalSignature.LegallyIdentifiable): LedgerTransaction { + val fullySigned = partialTX + tsaSig + ourSignature + val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) + + // TODO: We should run it through our full TransactionGroup of all transactions here. + + logger.trace { "Built finished transaction, sending back to secondary!" } + + send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(tsaSig, ourSignature)) + return ltx + } + } class UnacceptablePriceException(val givenPrice: Amount) : Exception() class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() { override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" } - // The buyer's side of the protocol. See note above Seller to learn about the caveats here. - class Buyer(val otherSide: SingleMessageRecipient, - val timestampingAuthority: Party, - val acceptablePrice: Amount, - val typeToBuy: Class, - val sessionID: Long) : ProtocolStateMachine>() { + open class Buyer(val otherSide: SingleMessageRecipient, + val timestampingAuthority: Party, + val acceptablePrice: Amount, + val typeToBuy: Class, + val sessionID: Long) : ProtocolStateMachine>() { @Suspendable override fun call(): Pair { + val tradeRequest = receiveAndValidateTradeRequest() + val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest) + val stx = signWithOurKeys(cashSigningPubKeys, ptx) + val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID) + + logger.trace { "Got signatures from seller, verifying ... "} + val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig + val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) + + logger.trace { "Fully signed transaction was valid. Trade complete! :-)" } + return Pair(fullySigned.tx, ltx) + } + + @Suspendable + open fun receiveAndValidateTradeRequest(): SellerTradeInfo { // Wait for a trade request to come in on our pre-provided session ID. - val tradeRequest = receive(TRADE_TOPIC, sessionID, SellerTradeInfo::class.java) + val maybeTradeRequest = receive(TRADE_TOPIC, sessionID, SellerTradeInfo::class.java) - // What is the seller trying to sell us? - val assetTypeName = tradeRequest.assetForSale.state.javaClass.name - logger.trace { "Got trade request for a $assetTypeName" } + val tradeRequest = maybeTradeRequest.validate { + // What is the seller trying to sell us? + val assetTypeName = it.assetForSale.state.javaClass.name + logger.trace { "Got trade request for a $assetTypeName" } - // Check the start message for acceptability. - check(tradeRequest.sessionID > 0) - if (tradeRequest.price > acceptablePrice) - throw UnacceptablePriceException(tradeRequest.price) - if (!typeToBuy.isInstance(tradeRequest.assetForSale.state)) - throw AssetMismatchException(typeToBuy.name, assetTypeName) + // Check the start message for acceptability. + check(it.sessionID > 0) + if (it.price > acceptablePrice) + throw UnacceptablePriceException(it.price) + if (!typeToBuy.isInstance(it.assetForSale.state)) + throw AssetMismatchException(typeToBuy.name, assetTypeName) + } // TODO: Either look up the stateref here in our local db, or accept a long chain of states and // validate them to audit the other side and ensure it actually owns the state we are being offered! // For now, just assume validity! + return tradeRequest + } - // Generate the shared transaction that both sides will sign, using the data we have. + @Suspendable + open fun swapSignaturesWithSeller(stx: SignedWireTransaction, theirSessionID: Long): SignaturesFromSeller { + logger.trace { "Sending partially signed transaction to seller" } + + // TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx. + + return sendAndReceive(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx, SignaturesFromSeller::class.java).validate {} + } + + open fun signWithOurKeys(cashSigningPubKeys: List, ptx: TransactionBuilder): SignedWireTransaction { + // Now sign the transaction with whatever keys we need to move the cash. + for (k in cashSigningPubKeys) { + val priv = serviceHub.keyManagementService.toPrivate(k) + ptx.signWith(KeyPair(k, priv)) + } + + val stx = ptx.toSignedTransaction(checkSufficientSignatures = false) + stx.verifySignatures() // Verifies that we generated a signed transaction correctly. + + // TODO: Could run verify() here to make sure the only signature missing is the sellers. + + return stx + } + + open fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> { val ptx = TransactionBuilder() // Add input and output states for the movement of cash, by using the Cash contract to generate the states. val wallet = serviceHub.walletService.currentWallet val cashStates = wallet.statesOfType() - val cashSigningPubKeys = Cash().craftSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates) + val cashSigningPubKeys = Cash().generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates) // Add inputs/outputs/a command for the movement of the asset. ptx.addInputState(tradeRequest.assetForSale.ref) // Just pick some new public key for now. This won't be linked with our identity in any way, which is what @@ -177,33 +242,7 @@ object TwoPartyTradeProtocol { // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. ptx.setTime(Instant.now(), timestampingAuthority, 30.seconds) - - // Now sign the transaction with whatever keys we need to move the cash. - for (k in cashSigningPubKeys) { - val priv = serviceHub.keyManagementService.toPrivate(k) - ptx.signWith(KeyPair(k, priv)) - } - - val stx = ptx.toSignedTransaction(checkSufficientSignatures = false) - stx.verifySignatures() // Verifies that we generated a signed transaction correctly. - - // TODO: Could run verify() here to make sure the only signature missing is the sellers. - - logger.trace { "Sending partially signed transaction to seller" } - - // TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx. - // TODO: Protect against a malicious buyer sending us back a different transaction to the one we built. - - val fullySigned = sendAndReceive(TRADE_TOPIC, otherSide, tradeRequest.sessionID, - sessionID, stx, SignedWireTransaction::class.java) - - logger.trace { "Got fully signed transaction, verifying ... "} - - val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService) - - logger.trace { "Fully signed transaction was valid. Trade complete! :-)" } - - return Pair(fullySigned.tx, ltx) + return Pair(ptx, cashSigningPubKeys) } } } \ No newline at end of file diff --git a/src/main/kotlin/core/Services.kt b/src/main/kotlin/core/Services.kt index 8630bd4509..d9dd1e6f68 100644 --- a/src/main/kotlin/core/Services.kt +++ b/src/main/kotlin/core/Services.kt @@ -9,11 +9,12 @@ package core import co.paralleluniverse.fibers.Suspendable +import core.crypto.DigitalSignature +import core.crypto.generateKeyPair import core.messaging.MessagingService import core.messaging.NetworkMap import core.serialization.SerializedBytes import java.security.KeyPair -import java.security.KeyPairGenerator import java.security.PrivateKey import java.security.PublicKey @@ -91,7 +92,7 @@ interface TimestamperService { // We define a dummy authority here to allow to us to develop prototype contracts in the absence of a real authority. // The timestamper itself is implemented in the unit test part of the code (in TestUtils.kt). object DummyTimestampingAuthority { - val key = KeyPairGenerator.getInstance("EC").genKeyPair() + val key = generateKeyPair() val identity = Party("The dummy timestamper", key.public) } diff --git a/src/main/kotlin/core/Structures.kt b/src/main/kotlin/core/Structures.kt index 66c92740d2..498e4ea4ef 100644 --- a/src/main/kotlin/core/Structures.kt +++ b/src/main/kotlin/core/Structures.kt @@ -8,6 +8,8 @@ package core +import core.crypto.SecureHash +import core.crypto.toStringShort import core.serialization.OpaqueBytes import core.serialization.serialize import java.security.PublicKey @@ -39,17 +41,16 @@ interface OwnableState : ContractState { /** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */ fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bits) -// TODO: Give this a shorter name. /** - * A stateref is a pointer to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which transaction - * defined the state and where in that transaction it was. + * A stateref is a pointer (reference) to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which + * transaction defined the state and where in that transaction it was. */ -data class ContractStateRef(val txhash: SecureHash, val index: Int) { +data class StateRef(val txhash: SecureHash, val index: Int) { override fun toString() = "$txhash($index)" } /** A StateAndRef is simply a (state, ref) pair. For instance, a wallet (which holds available assets) contains these. */ -data class StateAndRef(val state: T, val ref: ContractStateRef) +data class StateAndRef(val state: T, val ref: StateRef) /** A [Party] is well known (name, pubkey) pair. In a real system this would probably be an X.509 certificate. */ data class Party(val name: String, val owningKey: PublicKey) { @@ -133,3 +134,16 @@ interface Contract { */ val legalContractReference: SecureHash } + +/** A contract factory knows how to lazily load and instantiate contract objects. */ +interface ContractFactory { + /** + * Loads, instantiates and returns a contract object from its class bytecodes, given the hash of that bytecode. + * + * @throws UnknownContractException if the hash doesn't map to any known contract. + * @throws ClassCastException if the hash mapped to a contract, but it was not of type T + */ + operator fun get(hash: SecureHash): T +} + +class UnknownContractException : Exception() \ No newline at end of file diff --git a/src/main/kotlin/core/TransactionVerification.kt b/src/main/kotlin/core/TransactionVerification.kt index af879c47d5..69e5f4079a 100644 --- a/src/main/kotlin/core/TransactionVerification.kt +++ b/src/main/kotlin/core/TransactionVerification.kt @@ -8,10 +8,11 @@ package core +import core.crypto.SecureHash import java.util.* class TransactionResolutionException(val hash: SecureHash) : Exception() -class TransactionConflictException(val conflictRef: ContractStateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception() +class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception() /** * A TransactionGroup defines a directed acyclic graph of transactions that can be resolved with each other and then @@ -26,19 +27,19 @@ class TransactionGroup(val transactions: Set, val nonVerified /** * Verifies the group and returns the set of resolved transactions. */ - fun verify(programMap: Map): Set { + fun verify(programMap: ContractFactory): Set { // Check that every input can be resolved to an output. // Check that no output is referenced by more than one input. // Cycles should be impossible due to the use of hashes as pointers. check(transactions.intersect(nonVerifiedRoots).isEmpty()) val hashToTXMap: Map> = (transactions + nonVerifiedRoots).groupBy { it.hash } - val refToConsumingTXMap = hashMapOf() + val refToConsumingTXMap = hashMapOf() val resolved = HashSet(transactions.size) for (tx in transactions) { - val inputs = ArrayList(tx.inStateRefs.size) - for (ref in tx.inStateRefs) { + val inputs = ArrayList(tx.inputs.size) + for (ref in tx.inputs) { val conflict = refToConsumingTXMap[ref] if (conflict != null) throw TransactionConflictException(ref, tx, conflict) @@ -47,9 +48,9 @@ class TransactionGroup(val transactions: Set, val nonVerified // Look up the connecting transaction. val ltx = hashToTXMap[ref.txhash]?.single() ?: throw TransactionResolutionException(ref.txhash) // Look up the output in that transaction by index. - inputs.add(ltx.outStates[ref.index]) + inputs.add(ltx.outputs[ref.index]) } - resolved.add(TransactionForVerification(inputs, tx.outStates, tx.commands, tx.hash)) + resolved.add(TransactionForVerification(inputs, tx.outputs, tx.commands, tx.hash)) } for (tx in resolved) @@ -72,12 +73,12 @@ data class TransactionForVerification(val inStates: List, * @throws IllegalStateException if a state refers to an unknown contract. */ @Throws(TransactionVerificationException::class, IllegalStateException::class) - fun verify(programMap: Map) { + fun verify(programMap: ContractFactory) { // For each input and output state, locate the program to run. Then execute the verification function. If any // throws an exception, the entire transaction is invalid. val programHashes = (inStates.map { it.programRef } + outStates.map { it.programRef }).toSet() for (hash in programHashes) { - val program = programMap[hash] ?: throw IllegalStateException("Unknown program hash $hash") + val program: Contract = programMap[hash] try { program.verify(this) } catch(e: Throwable) { diff --git a/src/main/kotlin/core/Transactions.kt b/src/main/kotlin/core/Transactions.kt index 2c9974204e..4c4fc96170 100644 --- a/src/main/kotlin/core/Transactions.kt +++ b/src/main/kotlin/core/Transactions.kt @@ -9,6 +9,9 @@ package core import co.paralleluniverse.fibers.Suspendable +import core.crypto.DigitalSignature +import core.crypto.SecureHash +import core.crypto.signWithECDSA import core.node.TimestampingError import core.serialization.SerializedBytes import core.serialization.deserialize @@ -51,22 +54,22 @@ import java.util.* */ /** Transaction ready for serialisation, without any signatures attached. */ -data class WireTransaction(val inputStates: List, - val outputStates: List, +data class WireTransaction(val inputs: List, + val outputs: List, val commands: List) { fun toLedgerTransaction(identityService: IdentityService, originalHash: SecureHash): LedgerTransaction { val authenticatedArgs = commands.map { val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) } AuthenticatedObject(it.pubkeys, institutions, it.data) } - return LedgerTransaction(inputStates, outputStates, authenticatedArgs, originalHash) + return LedgerTransaction(inputs, outputs, authenticatedArgs, originalHash) } override fun toString(): String { val buf = StringBuilder() buf.appendln("Transaction:") - for (input in inputStates) buf.appendln("${Emoji.rightArrow}INPUT: $input") - for (output in outputStates) buf.appendln("${Emoji.leftArrow}OUTPUT: $output") + for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input") + for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output") for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command") return buf.toString() } @@ -76,7 +79,7 @@ data class WireTransaction(val inputStates: List, data class SignedWireTransaction(val txBits: SerializedBytes, val sigs: List) { init { check(sigs.isNotEmpty()) } - // Lazily calculated access to the deserialised/hashed transaction data. + /** Lazily calculated access to the deserialised/hashed transaction data. */ val tx: WireTransaction by lazy { txBits.deserialize() } /** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */ @@ -121,11 +124,14 @@ data class SignedWireTransaction(val txBits: SerializedBytes, v /** Returns the same transaction but with an additional (unchecked) signature */ fun withAdditionalSignature(sig: DigitalSignature.WithKey) = copy(sigs = sigs + sig) + + /** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */ + operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig) } /** A mutable transaction that's in the process of being built, before all signatures are present. */ -class TransactionBuilder(private val inputStates: MutableList = arrayListOf(), - private val outputStates: MutableList = arrayListOf(), +class TransactionBuilder(private val inputs: MutableList = arrayListOf(), + private val outputs: MutableList = arrayListOf(), private val commands: MutableList = arrayListOf()) { val time: TimestampCommand? get() = commands.mapNotNull { it.data as? TimestampCommand }.singleOrNull() @@ -152,8 +158,8 @@ class TransactionBuilder(private val inputStates: MutableList public fun withItems(vararg items: Any): TransactionBuilder { for (t in items) { when (t) { - is ContractStateRef -> inputStates.add(t) - is ContractState -> outputStates.add(t) + is StateRef -> inputs.add(t) + is ContractState -> outputs.add(t) is Command -> commands.add(t) else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") } @@ -211,7 +217,7 @@ class TransactionBuilder(private val inputStates: MutableList currentSigs.add(sig) } - fun toWireTransaction() = WireTransaction(ArrayList(inputStates), ArrayList(outputStates), ArrayList(commands)) + fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(outputs), ArrayList(commands)) fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedWireTransaction { if (checkSufficientSignatures) { @@ -224,14 +230,14 @@ class TransactionBuilder(private val inputStates: MutableList return SignedWireTransaction(toWireTransaction().serialize(), ArrayList(currentSigs)) } - fun addInputState(ref: ContractStateRef) { + fun addInputState(ref: StateRef) { check(currentSigs.isEmpty()) - inputStates.add(ref) + inputs.add(ref) } fun addOutputState(state: ContractState) { check(currentSigs.isEmpty()) - outputStates.add(state) + outputs.add(state) } fun addCommand(arg: Command) { @@ -245,8 +251,8 @@ class TransactionBuilder(private val inputStates: MutableList fun addCommand(data: CommandData, keys: List) = addCommand(Command(data, keys)) // Accessors that yield immutable snapshots. - fun inputStates(): List = ArrayList(inputStates) - fun outputStates(): List = ArrayList(outputStates) + fun inputStates(): List = ArrayList(inputs) + fun outputStates(): List = ArrayList(outputs) fun commands(): List = ArrayList(commands) } @@ -256,20 +262,20 @@ class TransactionBuilder(private val inputStates: MutableList * with the commands from the wire, and verified/looked up. */ data class LedgerTransaction( - /** The input states which will be consumed/invalidated by the execution of this transaction. */ - val inStateRefs: List, - /** The states that will be generated by the execution of this transaction. */ - val outStates: List, - /** Arbitrary data passed to the program of each input state. */ + /** The input states which will be consumed/invalidated by the execution of this transaction. */ + val inputs: List, + /** The states that will be generated by the execution of this transaction. */ + val outputs: List, + /** Arbitrary data passed to the program of each input state. */ val commands: List>, - /** The hash of the original serialised SignedTransaction */ + /** The hash of the original serialised SignedTransaction */ val hash: SecureHash ) { @Suppress("UNCHECKED_CAST") - fun outRef(index: Int) = StateAndRef(outStates[index] as T, ContractStateRef(hash, index)) + fun outRef(index: Int) = StateAndRef(outputs[index] as T, StateRef(hash, index)) fun outRef(state: T): StateAndRef { - val i = outStates.indexOf(state) + val i = outputs.indexOf(state) if (i == -1) throw IllegalArgumentException("State not found in this transaction") return outRef(i) diff --git a/src/main/kotlin/core/messaging/InMemoryNetwork.kt b/src/main/kotlin/core/messaging/InMemoryNetwork.kt index 143d1a2d7e..80180384ec 100644 --- a/src/main/kotlin/core/messaging/InMemoryNetwork.kt +++ b/src/main/kotlin/core/messaging/InMemoryNetwork.kt @@ -12,10 +12,10 @@ 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.crypto.sha256 import core.node.TimestamperNodeService -import core.sha256 import core.utilities.loggerFor -import java.security.KeyPairGenerator import java.time.Instant import java.util.* import java.util.concurrent.Executor @@ -25,8 +25,8 @@ import javax.annotation.concurrent.ThreadSafe import kotlin.concurrent.thread /** - * An in-memory network allows you to manufacture [Node]s for a set of participants. Each - * [Node] maintains a queue of messages it has received, and a background thread that dispatches + * An in-memory network allows you to manufacture [InMemoryNode]s for a set of participants. Each + * [InMemoryNode] maintains a queue of messages it has received, and a background thread that dispatches * messages one by one to registered handlers. Alternatively, a messaging system may be manually pumped, in which * case no thread is created and a caller is expected to force delivery one at a time (this is useful for unit * testing). @@ -34,26 +34,26 @@ import kotlin.concurrent.thread @ThreadSafe public class InMemoryNetwork { private var counter = 0 // -1 means stopped. - private val handleNodeMap = HashMap() + private val handleNodeMap = HashMap() // All messages are kept here until the messages are pumped off the queue by a caller to the node class. // Queues are created on-demand when a message is sent to an address: the receiving node doesn't have to have // been created yet. If the node identified by the given handle has gone away/been shut down then messages // stack up here waiting for it to come back. The intent of this is to simulate a reliable messaging network. private val messageQueues = HashMap>() - val nodes: List @Synchronized get() = handleNodeMap.values.toList() + val nodes: List @Synchronized get() = handleNodeMap.values.toList() /** * Creates a node and returns the new object that identifies its location on the network to senders, and the - * [Node] that the recipient/in-memory node uses to receive messages and send messages itself. + * [InMemoryNode] that the recipient/in-memory node uses to receive messages and send messages itself. * - * If [manuallyPumped] is set to true, then you are expected to call the [Node.pump] method on the [Node] + * If [manuallyPumped] is set to true, then you are expected to call the [InMemoryNode.pump] method on the [InMemoryNode] * in order to cause the delivery of a single message, which will occur on the thread of the caller. If set to false * then this class will set up a background thread to deliver messages asynchronously, if the handler specifies no * executor. */ @Synchronized - fun createNode(manuallyPumped: Boolean): Pair> { + fun createNode(manuallyPumped: Boolean): Pair> { check(counter >= 0) { "In memory network stopped: please recreate. "} val builder = createNodeWithID(manuallyPumped, counter) as Builder counter++ @@ -62,7 +62,7 @@ public class InMemoryNetwork { } /** Creates a node at the given address: useful if you want to recreate a node to simulate a restart */ - fun createNodeWithID(manuallyPumped: Boolean, id: Int): MessagingServiceBuilder { + fun createNodeWithID(manuallyPumped: Boolean, id: Int): MessagingServiceBuilder { return Builder(manuallyPumped, Handle(id)) } @@ -103,10 +103,10 @@ public class InMemoryNetwork { messageQueues.clear() } - inner class Builder(val manuallyPumped: Boolean, val id: Handle) : MessagingServiceBuilder { - override fun start(): ListenableFuture { + inner class Builder(val manuallyPumped: Boolean, val id: Handle) : MessagingServiceBuilder { + override fun start(): ListenableFuture { synchronized(this@InMemoryNetwork) { - val node = Node(manuallyPumped, id) + val node = InMemoryNode(manuallyPumped, id) handleNodeMap[id] = node return Futures.immediateFuture(node) } @@ -122,11 +122,11 @@ public class InMemoryNetwork { private var timestampingAdvert: LegallyIdentifiableNode? = null @Synchronized - fun setupTimestampingNode(manuallyPumped: Boolean): Pair { + fun setupTimestampingNode(manuallyPumped: Boolean): Pair { check(timestampingAdvert == null) val (handle, builder) = createNode(manuallyPumped) val node = builder.start().get() - val key = KeyPairGenerator.getInstance("EC").genKeyPair() + val key = generateKeyPair() val identity = Party("Unit test timestamping authority", key.public) TimestamperNodeService(node, identity, key) timestampingAdvert = LegallyIdentifiableNode(handle, identity) @@ -134,13 +134,13 @@ public class InMemoryNetwork { } /** - * An [Node] provides a [MessagingService] that isn't backed by any kind of network or disk storage + * An [InMemoryNode] provides a [MessagingService] that isn't backed by any kind of network or disk storage * system, but just uses regular queues on the heap instead. It is intended for unit testing and developer convenience * when all entities on 'the network' are being simulated in-process. * * An instance can be obtained by creating a builder and then using the start method. */ - inner class Node(private val manuallyPumped: Boolean, private val handle: Handle): MessagingService { + inner class InMemoryNode(private val manuallyPumped: Boolean, private val handle: Handle): MessagingService { inner class Handler(val executor: Executor?, val topic: String, val callback: (Message, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration @GuardedBy("this") diff --git a/src/main/kotlin/core/messaging/StateMachines.kt b/src/main/kotlin/core/messaging/StateMachines.kt index 6576972788..44e8357d96 100644 --- a/src/main/kotlin/core/messaging/StateMachines.kt +++ b/src/main/kotlin/core/messaging/StateMachines.kt @@ -17,13 +17,13 @@ import com.esotericsoftware.kryo.io.Output import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.SettableFuture -import core.SecureHash import core.ServiceHub +import core.crypto.SecureHash +import core.crypto.sha256 import core.serialization.THREAD_LOCAL_KRYO import core.serialization.createKryo import core.serialization.deserialize import core.serialization.serialize -import core.sha256 import core.utilities.trace import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -59,6 +59,15 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) // property. private val _stateMachines = Collections.synchronizedList(ArrayList>()) + // This is a workaround for something Gradle does to us during unit tests. It replaces stderr with its own + // class that inserts itself into a ThreadLocal. That then gets caught in fiber serialisation, which we don't + // want because it can't get recreated properly. It turns out there's no good workaround for this! All the obvious + // approaches fail. Pending resolution of https://github.com/puniverse/quasar/issues/153 we just disable + // checkpointing when unit tests are run inside Gradle. The right fix is probably to make Quasar's + // bit-too-clever-for-its-own-good ThreadLocal serialisation trick. It already wasted far more time than it can + // ever recover. + val checkpointing: Boolean get() = !System.err.javaClass.name.contains("LinePerThreadBufferingOutputStream") + /** Returns a snapshot of the currently registered state machines. */ val stateMachines: List> get() { synchronized(_stateMachines) { @@ -82,7 +91,8 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) ) init { - restoreCheckpoints() + if (checkpointing) + restoreCheckpoints() } /** Reads the database map and resurrects any serialised state machines. */ @@ -118,8 +128,7 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) } /** - * Kicks off a brand new state machine of the given class. It will log with the named logger, and the - * [initialArgs] object will be passed to the call method of the [ProtocolStateMachine] object. + * Kicks off a brand new state machine of the given class. It will log with the named logger. * The state machine will be persisted when it suspends, with automated restart if the StateMachineManager is * restarted with checkpointed state machines in the storage service. */ @@ -166,19 +175,12 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) psm.prepareForResumeWith(serviceHub, obj, logger, onSuspend) - try { - // Now either start or carry on with the protocol from where it left off (or at the start). - resumeFunc(psm) + resumeFunc(psm) - // We're back! Check if the fiber is finished and if so, clean up. - if (psm.isTerminated) { - _stateMachines.remove(psm) - checkpointsMap.remove(prevCheckpointKey) - } - } catch (t: Throwable) { - // TODO: Quasar is logging exceptions by itself too, find out where and stop it. - logger.error("Caught error whilst invoking protocol state machine", t) - throw t + // We're back! Check if the fiber is finished and if so, clean up. + if (psm.isTerminated) { + _stateMachines.remove(psm) + checkpointsMap.remove(prevCheckpointKey) } } @@ -187,7 +189,8 @@ class StateMachineManager(val serviceHub: ServiceHub, val runInThread: Executor) serialisedFiber: ByteArray) { val checkpoint = Checkpoint(serialisedFiber, logger.name, topic, responseType.name) val curPersistedBytes = checkpoint.serialize().bits - persistCheckpoint(prevCheckpointKey, curPersistedBytes) + if (checkpointing) + persistCheckpoint(prevCheckpointKey, curPersistedBytes) val newCheckpointKey = curPersistedBytes.sha256() net.runOnNextMessage(topic, runInThread) { netMsg -> val obj: Any = THREAD_LOCAL_KRYO.get().readObject(Input(netMsg.data), responseType) @@ -234,6 +237,12 @@ abstract class ProtocolStateMachine : Fiber("protocol", SameThreadFiberSch @Transient protected lateinit var logger: Logger @Transient private var _resultFuture: SettableFuture? = SettableFuture.create() + init { + setDefaultUncaughtExceptionHandler { strand, throwable -> + logger.error("Caught error whilst running protocol state machine ${this.javaClass.name}", throwable) + } + } + /** This future will complete when the call method returns. */ val resultFuture: ListenableFuture get() { return _resultFuture ?: run { @@ -263,7 +272,7 @@ abstract class ProtocolStateMachine : Fiber("protocol", SameThreadFiberSch } @Suspendable @Suppress("UNCHECKED_CAST") - private fun suspendAndExpectReceive(with: FiberRequest): T { + private fun suspendAndExpectReceive(with: FiberRequest): UntrustworthyData { Fiber.parkAndSerialize { fiber, serializer -> // We don't use the passed-in serializer here, because we need to use our own augmented Kryo. val deserializer = Fiber.getFiberSerializer() as KryoSerializer @@ -276,18 +285,18 @@ abstract class ProtocolStateMachine : Fiber("protocol", SameThreadFiberSch } val tmp = resumeWithObject ?: throw IllegalStateException("Expected to receive something") resumeWithObject = null - return tmp as T + return UntrustworthyData(tmp as T) } @Suspendable @Suppress("UNCHECKED_CAST") fun sendAndReceive(topic: String, destination: MessageRecipients, sessionIDForSend: Long, sessionIDForReceive: Long, - obj: Any, recvType: Class): T { + obj: Any, recvType: Class): UntrustworthyData { val result = FiberRequest.ExpectingResponse(topic, destination, sessionIDForSend, sessionIDForReceive, obj, recvType) return suspendAndExpectReceive(result) } @Suspendable - fun receive(topic: String, sessionIDForReceive: Long, recvType: Class): T { + fun receive(topic: String, sessionIDForReceive: Long, recvType: Class): UntrustworthyData { val result = FiberRequest.ExpectingResponse(topic, null, -1, sessionIDForReceive, null, recvType) return suspendAndExpectReceive(result) } @@ -299,6 +308,29 @@ abstract class ProtocolStateMachine : Fiber("protocol", SameThreadFiberSch } } +/** + * A small utility to approximate taint tracking: if a method gives you back one of these, it means the data came from + * a remote source that may be incentivised to pass us junk that violates basic assumptions and thus must be checked + * first. The wrapper helps you to avoid forgetting this vital step. Things you might want to check are: + * + * - Is this object the one you actually expected? Did the other side hand you back something technically valid but + * not what you asked for? + * - Is the object disobeying its own invariants? + * - Are any objects *reachable* from this object mismatched or not what you expected? + * - Is it suspiciously large or small? + */ +class UntrustworthyData(private val fromUntrustedWorld: T) { + val data: T + @Deprecated("Accessing the untrustworthy data directly without validating it first is a bad idea") + get() = fromUntrustedWorld + + @Suppress("DEPRECATION") + inline fun validate(validator: (T) -> Unit): T { + validator(data) + return data + } +} + // TODO: Clean this up open class FiberRequest(val topic: String, val destination: MessageRecipients?, val sessionIDForSend: Long, val sessionIDForReceive: Long, val obj: Any?) { diff --git a/src/main/kotlin/core/node/ArtemisMessagingService.kt b/src/main/kotlin/core/node/ArtemisMessagingService.kt index 54c4cdaf91..1eb835eec5 100644 --- a/src/main/kotlin/core/node/ArtemisMessagingService.kt +++ b/src/main/kotlin/core/node/ArtemisMessagingService.kt @@ -111,11 +111,16 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort) secConfig.addUser("internal", password) secConfig.addRole("internal", "internal") secConfig.defaultUser = "internal" - config.setSecurityRoles(mapOf( + config.securityRoles = mapOf( "#" to setOf(Role("internal", true, true, true, true, true, true, true)) - )) + ) val secManager = ActiveMQJAASSecurityManager(InVMLoginModule::class.java.name, secConfig) mq.setSecurityManager(secManager) + + // Currently we cannot find out if something goes wrong during startup :( This is bug ARTEMIS-388 filed by me. + // The fix should be in the 1.3.0 release: + // + // https://issues.apache.org/jira/browse/ARTEMIS-388 mq.start() // Connect to our in-memory server. @@ -242,19 +247,19 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort) hostAndPort.hostText, hostAndPort.port)) mq.activeMQServer.deployBridge(BridgeConfiguration().apply { setName(name) - setQueueName(name) - setForwardingAddress(name) - setStaticConnectors(listOf(name)) - setConfirmationWindowSize(100000) // a guess + queueName = name + forwardingAddress = name + staticConnectors = listOf(name) + confirmationWindowSize = 100000 // a guess }) } } private fun setConfigDirectories(config: Configuration, dir: Path) { config.apply { - setBindingsDirectory(dir.resolve("bindings").toString()) - setJournalDirectory(dir.resolve("journal").toString()) - setLargeMessagesDirectory(dir.resolve("largemessages").toString()) + bindingsDirectory = dir.resolve("bindings").toString() + journalDirectory = dir.resolve("journal").toString() + largeMessagesDirectory = dir.resolve("largemessages").toString() } } @@ -262,11 +267,9 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort) val config = ConfigurationImpl() setConfigDirectories(config, directory) // We will be talking to our server purely in memory. - config.setAcceptorConfigurations( - setOf( - tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port), - TransportConfiguration(InVMAcceptorFactory::class.java.name) - ) + config.acceptorConfigurations = setOf( + tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port), + TransportConfiguration(InVMAcceptorFactory::class.java.name) ) return config } diff --git a/src/main/kotlin/core/node/E2ETestKeyManagementService.kt b/src/main/kotlin/core/node/E2ETestKeyManagementService.kt index efebd5bdc1..9943454736 100644 --- a/src/main/kotlin/core/node/E2ETestKeyManagementService.kt +++ b/src/main/kotlin/core/node/E2ETestKeyManagementService.kt @@ -10,8 +10,8 @@ package core.node import core.KeyManagementService import core.ThreadBox +import core.crypto.generateKeyPair import java.security.KeyPair -import java.security.KeyPairGenerator import java.security.PrivateKey import java.security.PublicKey import java.util.* @@ -39,7 +39,7 @@ class E2ETestKeyManagementService : KeyManagementService { override val keys: Map get() = mutex.locked { HashMap(keys) } override fun freshKey(): KeyPair { - val keypair = KeyPairGenerator.getInstance("EC").genKeyPair() + val keypair = generateKeyPair() mutex.locked { keys[keypair.public] = keypair.private } diff --git a/src/main/kotlin/core/node/E2ETestWalletService.kt b/src/main/kotlin/core/node/E2ETestWalletService.kt index f626188311..8bdd61c90c 100644 --- a/src/main/kotlin/core/node/E2ETestWalletService.kt +++ b/src/main/kotlin/core/node/E2ETestWalletService.kt @@ -53,14 +53,14 @@ class E2ETestWalletService(private val services: ServiceHub) : WalletService { val issuance = TransactionBuilder() val freshKey = services.keyManagementService.freshKey() - cash.craftIssue(issuance, Amount(pennies, howMuch.currency), depositRef, freshKey.public) + cash.generateIssue(issuance, Amount(pennies, howMuch.currency), depositRef, freshKey.public) issuance.signWith(myKey) return@map issuance.toSignedTransaction(true) } val statesAndRefs = transactions.map { - StateAndRef(it.tx.outputStates[0] as OwnableState, ContractStateRef(it.id, 0)) + StateAndRef(it.tx.outputs[0] as OwnableState, StateRef(it.id, 0)) } mutex.locked { diff --git a/src/main/kotlin/core/node/Node.kt b/src/main/kotlin/core/node/Node.kt index 1ad56c0106..89e3d885f6 100644 --- a/src/main/kotlin/core/node/Node.kt +++ b/src/main/kotlin/core/node/Node.kt @@ -10,19 +10,21 @@ package core.node import com.google.common.net.HostAndPort import core.* +import core.crypto.generateKeyPair import core.messaging.* import core.serialization.deserialize import core.serialization.serialize import core.utilities.loggerFor +import java.io.RandomAccessFile +import java.lang.management.ManagementFactory +import java.nio.channels.FileLock import java.nio.file.Files import java.nio.file.Path +import java.nio.file.StandardOpenOption import java.security.KeyPair -import java.security.KeyPairGenerator import java.util.* import java.util.concurrent.Executors -val DEFAULT_PORT = 31337 - class ConfigurationException(message: String) : Exception(message) // TODO: Split this into a regression testing environment @@ -65,17 +67,27 @@ class Node(val dir: Path, val myNetAddr: HostAndPort, val configuration: NodeCon override val identityService: IdentityService get() = identity } - // TODO: Implement mutual exclusion so we can't start the node twice by accident. - - val storage = makeStorageService(dir) - val smm = StateMachineManager(services, serverThread) - val net = ArtemisMessagingService(dir, myNetAddr) - val wallet: WalletService = E2ETestWalletService(services) - val keyManagement = E2ETestKeyManagementService() + val storage: StorageService + val smm: StateMachineManager + val net: ArtemisMessagingService + val wallet: WalletService + val keyManagement: E2ETestKeyManagementService val inNodeTimestampingService: TimestamperNodeService? val identity: IdentityService + // Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us + // when our process shuts down, but we try in stop() anyway just to be nice. + private var nodeFileLock: FileLock? = null + init { + alreadyRunningNodeCheck() + + storage = makeStorageService(dir) + smm = StateMachineManager(services, serverThread) + net = ArtemisMessagingService(dir, myNetAddr) + wallet = E2ETestWalletService(services) + keyManagement = E2ETestKeyManagementService() + // Insert a network map entry for the timestamper: this is all temp scaffolding and will go away. If we are // given the details, the timestamping node is somewhere else. Otherwise, we do our own timestamping. val tsid = if (timestamperAddress != null) { @@ -101,6 +113,7 @@ class Node(val dir: Path, val myNetAddr: HostAndPort, val configuration: NodeCon fun stop() { net.stop() serverThread.shutdownNow() + nodeFileLock!!.release() } fun makeStorageService(dir: Path): StorageService { @@ -114,7 +127,7 @@ class Node(val dir: Path, val myNetAddr: HostAndPort, val configuration: NodeCon val (identity, keypair) = if (!Files.exists(privKeyFile)) { log.info("Identity key not found, generating fresh key!") - val keypair: KeyPair = KeyPairGenerator.getInstance("EC").genKeyPair() + val keypair: KeyPair = generateKeyPair() keypair.serialize().writeToFile(privKeyFile) val myIdentity = Party(configuration.myLegalName, keypair.public) // We include the Party class with the file here to help catch mixups when admins provide files of the @@ -152,8 +165,35 @@ class Node(val dir: Path, val myNetAddr: HostAndPort, val configuration: NodeCon } } + private fun alreadyRunningNodeCheck() { + // Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a + // file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already + // exists, we try to take the file lock first before replacing it and if that fails it means we're being started + // twice with the same directory: that's a user error and we should bail out. + val pidPath = dir.resolve("process-id") + val file = pidPath.toFile() + if (file.exists()) { + val f = RandomAccessFile(file, "rw") + val l = f.channel.tryLock() + if (l == null) { + println("It appears there is already a node running with the specified data directory $dir") + println("Shut that other node down and try again. It may have process ID ${file.readText()}") + System.exit(1) + } + nodeFileLock = l + } + val ourProcessID: String = ManagementFactory.getRuntimeMXBean().name.split("@")[0] + Files.write(pidPath, ourProcessID.toByteArray(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) + pidPath.toFile().deleteOnExit() + if (nodeFileLock == null) + nodeFileLock = RandomAccessFile(file, "rw").channel.lock() + } + companion object { val PRIVATE_KEY_FILE_NAME = "identity-private-key" val PUBLIC_IDENTITY_FILE_NAME = "identity-public" + + /** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */ + val DEFAULT_PORT = 31337 } } \ No newline at end of file diff --git a/src/main/kotlin/core/node/TimestamperNodeService.kt b/src/main/kotlin/core/node/TimestamperNodeService.kt index 535698727e..433cc7a480 100644 --- a/src/main/kotlin/core/node/TimestamperNodeService.kt +++ b/src/main/kotlin/core/node/TimestamperNodeService.kt @@ -11,6 +11,8 @@ package core.node import co.paralleluniverse.common.util.VisibleForTesting import co.paralleluniverse.fibers.Suspendable import core.* +import core.crypto.DigitalSignature +import core.crypto.signWithECDSA import core.messaging.LegallyIdentifiableNode import core.messaging.MessageRecipients import core.messaging.MessagingService @@ -115,10 +117,13 @@ class TimestamperClient(private val psm: ProtocolStateMachine<*>, private val no val sessionID = random63BitValue() val replyTopic = "${TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC}.$sessionID" val req = TimestampingMessages.Request(wtxBytes, psm.serviceHub.networkService.myAddress, replyTopic) - val signature = psm.sendAndReceive(TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC, node.address, 0, + + val maybeSignature = psm.sendAndReceive(TimestamperNodeService.TIMESTAMPING_PROTOCOL_TOPIC, node.address, 0, sessionID, req, DigitalSignature.LegallyIdentifiable::class.java) + // Check that the timestamping authority gave us back a valid signature and didn't break somehow - signature.verifyWithECDSA(wtxBytes) + val signature = maybeSignature.validate { it.verifyWithECDSA(wtxBytes) } + return signature } } diff --git a/src/main/kotlin/core/node/TraderDemo.kt b/src/main/kotlin/core/node/TraderDemo.kt index 91eb2bff69..01c84d6fcd 100644 --- a/src/main/kotlin/core/node/TraderDemo.kt +++ b/src/main/kotlin/core/node/TraderDemo.kt @@ -12,6 +12,8 @@ import com.google.common.net.HostAndPort import contracts.CommercialPaper import contracts.protocols.TwoPartyTradeProtocol import core.* +import core.crypto.SecureHash +import core.crypto.generateKeyPair import core.messaging.LegallyIdentifiableNode import core.messaging.SingleMessageRecipient import core.messaging.runOnNextMessage @@ -23,31 +25,14 @@ import joptsimple.OptionParser import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths -import java.security.KeyPairGenerator import java.security.PublicKey import java.time.Instant import java.util.* +import kotlin.system.exitProcess // TRADING DEMO // -// This demo app can be run in one of two modes. In the listening mode it will buy commercial paper from a selling node -// that connects to it, using IOU cash it issued to itself. It also runs a timestamping service in this mode. In the -// non-listening mode, it will connect to the specified listening node and sell some commercial paper in return for -// cash. There's currently no UI so all you can see is log messages. -// -// Please note that the software currently assumes every node has a unique DNS name. Thus you cannot name both nodes -// "localhost". This might get fixed in future, but for now to run the listening node, alias "alpha" to "localhost" -// in your /etc/hosts file and then try a command line like this: -// -// --dir=alpha --service-fake-trades --network-address=alpha -// -// To run the node that initiates a trade, alias "beta" to "localhost" in your /etc/hosts file and then try a command -// line like this: -// -// --dir=beta --fake-trade-with=alpha --network-address=beta:31338 -// --timestamper-identity-file=alpha/identity-public --timestamper-address=alpha -// -// Alternatively, +// Please see docs/build/html/running-the-trading-demo.html fun main(args: Array) { @@ -73,8 +58,7 @@ fun main(args: Array) { } catch (e: Exception) { println(e.message) printHelp() - System.exit(1) - throw Exception() // TODO: Remove when upgrading to Kotlin 1.0 RC + exitProcess(1) } BriefLogFormatter.initVerbose("platform.trade") @@ -88,11 +72,11 @@ fun main(args: Array) { val config = loadConfigFile(configFile) - val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(DEFAULT_PORT) + val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(Node.DEFAULT_PORT) val listening = options.has(serviceFakeTradesArg) val timestamperId = if (options.has(timestamperIdentityFile)) { - val addr = HostAndPort.fromString(options.valueOf(timestamperNetAddr)).withDefaultPort(DEFAULT_PORT) + val addr = HostAndPort.fromString(options.valueOf(timestamperNetAddr)).withDefaultPort(Node.DEFAULT_PORT) val path = Paths.get(options.valueOf(timestamperIdentityFile)) val party = Files.readAllBytes(path).deserialize(includeClassName = true) LegallyIdentifiableNode(ArtemisMessagingService.makeRecipient(addr), party) @@ -140,9 +124,9 @@ fun main(args: Array) { // Grab a session ID for the fake trade from the other side, then kick off the seller and sell them some junk. if (!options.has(fakeTradeWithArg)) { println("Need the --fake-trade-with command line argument") - System.exit(1) + exitProcess(1) } - val peerAddr = HostAndPort.fromString(options.valuesOf(fakeTradeWithArg).single()).withDefaultPort(DEFAULT_PORT) + val peerAddr = HostAndPort.fromString(options.valuesOf(fakeTradeWithArg).single()).withDefaultPort(Node.DEFAULT_PORT) val otherSide = ArtemisMessagingService.makeRecipient(peerAddr) node.net.runOnNextMessage("test.junktrade.initiate") { msg -> val sessionID = msg.data.deserialize() @@ -175,10 +159,10 @@ fun main(args: Array) { fun makeFakeCommercialPaper(ownedBy: PublicKey): StateAndRef { // Make a fake company that's issued its own paper. - val party = Party("MegaCorp, Inc", KeyPairGenerator.getInstance("EC").genKeyPair().public) + val party = Party("MegaCorp, Inc", generateKeyPair().public) // ownedBy here is the random key that gives us control over it. val paper = CommercialPaper.State(party.ref(1,2,3), ownedBy, 1100.DOLLARS, Instant.now() + 10.days) - val randomRef = ContractStateRef(SecureHash.randomSHA256(), 0) + val randomRef = StateRef(SecureHash.randomSHA256(), 0) return StateAndRef(paper, randomRef) } @@ -187,7 +171,7 @@ private fun loadConfigFile(configFile: Path): NodeConfiguration { println() println("This is the first run, so you should edit the config file in $configFile and then start the node again.") println() - System.exit(1) + exitProcess(1) } val defaultLegalName = "Global MegaCorp, Ltd." diff --git a/src/main/kotlin/core/serialization/Kryo.kt b/src/main/kotlin/core/serialization/Kryo.kt index e5acdd7abb..0cbb28054d 100644 --- a/src/main/kotlin/core/serialization/Kryo.kt +++ b/src/main/kotlin/core/serialization/Kryo.kt @@ -16,16 +16,16 @@ 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.SecureHash import core.SignedWireTransaction -import core.sha256 +import core.crypto.SecureHash +import core.crypto.generateKeyPair +import core.crypto.sha256 import de.javakaffee.kryoserializers.ArraysAsListSerializer import org.objenesis.strategy.StdInstantiatorStrategy import java.io.ByteArrayOutputStream import java.lang.reflect.InvocationTargetException import java.nio.file.Files import java.nio.file.Path -import java.security.KeyPairGenerator import java.time.Instant import java.util.* import kotlin.reflect.* @@ -198,7 +198,7 @@ fun createKryo(k: Kryo = Kryo()): Kryo { // Some things where the JRE provides an efficient custom serialisation. val ser = JavaSerializer() - val keyPair = KeyPairGenerator.getInstance("EC").genKeyPair() + val keyPair = generateKeyPair() register(keyPair.public.javaClass, ser) register(keyPair.private.javaClass, ser) register(Instant::class.java, ser) diff --git a/src/test/kotlin/contracts/CashTests.kt b/src/test/kotlin/contracts/CashTests.kt index 1c94ee8468..d238bccc54 100644 --- a/src/test/kotlin/contracts/CashTests.kt +++ b/src/test/kotlin/contracts/CashTests.kt @@ -6,14 +6,11 @@ * All other rights reserved. */ -/* - * Copyright 2015, R3 CEV. All rights reserved. - */ - import contracts.Cash import contracts.DummyContract import contracts.InsufficientBalanceException import core.* +import core.crypto.SecureHash import core.serialization.OpaqueBytes import core.testutils.* import org.junit.Test @@ -31,7 +28,6 @@ class CashTests { ) val outState = inState.copy(owner = DUMMY_PUBKEY_2) - fun Cash.State.editInstitution(party: Party) = copy(deposit = deposit.copy(party = party)) fun Cash.State.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref))) @Test @@ -56,7 +52,7 @@ class CashTests { } tweak { output { outState } - output { outState.editInstitution(MINI_CORP) } + output { outState `issued by` MINI_CORP } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } this `fails requirement` "at least one cash input" } @@ -104,7 +100,7 @@ class CashTests { } val ptx = TransactionBuilder() - Cash().craftIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12,34), owner = DUMMY_PUBKEY_1) + Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12,34), owner = DUMMY_PUBKEY_1) assertTrue(ptx.inputStates().isEmpty()) val s = ptx.outputStates()[0] as Cash.State assertEquals(100.DOLLARS, s.amount) @@ -156,7 +152,7 @@ class CashTests { // Can't change issuer. transaction { input { inState } - output { outState.editInstitution(MINI_CORP) } + output { outState `issued by` MINI_CORP } this `fails requirement` "at issuer MegaCorp the amounts balance" } // Can't change deposit reference when splitting. @@ -187,7 +183,7 @@ class CashTests { // Can't have superfluous input states from different issuers. transaction { input { inState } - input { inState.editInstitution(MINI_CORP) } + input { inState `issued by` MINI_CORP } output { outState } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } this `fails requirement` "at issuer MiniCorp the amounts balance" @@ -227,9 +223,9 @@ class CashTests { // Multi-issuer case. transaction { input { inState } - input { inState.editInstitution(MINI_CORP) } + input { inState `issued by` MINI_CORP } - output { inState.copy(amount = inState.amount - 200.DOLLARS).editInstitution(MINI_CORP) } + output { inState.copy(amount = inState.amount - 200.DOLLARS) `issued by` MINI_CORP } output { inState.copy(amount = inState.amount - 200.DOLLARS) } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } @@ -249,7 +245,7 @@ class CashTests { transaction { // Gather 2000 dollars from two different issuers. input { inState } - input { inState.editInstitution(MINI_CORP) } + input { inState `issued by` MINI_CORP } // Can't merge them together. tweak { @@ -265,7 +261,7 @@ class CashTests { // This works. output { inState.copy(owner = DUMMY_PUBKEY_2) } - output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) } + output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } this.accepts() } @@ -293,7 +289,7 @@ class CashTests { /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - // Spend crafting + // Spend tx generation val OUR_PUBKEY_1 = DUMMY_PUBKEY_1 val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2 @@ -301,7 +297,7 @@ class CashTests { fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = StateAndRef( Cash.State(corp.ref(depositRef), amount, OUR_PUBKEY_1), - ContractStateRef(SecureHash.randomSHA256(), Random().nextInt(32)) + StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) val WALLET = listOf( @@ -313,56 +309,56 @@ class CashTests { fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction { val tx = TransactionBuilder() - Cash().craftSpend(tx, amount, dest, WALLET) + Cash().generateSpend(tx, amount, dest, WALLET) return tx.toWireTransaction() } @Test - fun craftSimpleDirectSpend() { + fun generateSimpleDirectSpend() { val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputStates[0]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputStates[0]) + assertEquals(WALLET[0].ref, wtx.inputs[0]) + assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0]) assertEquals(OUR_PUBKEY_1, wtx.commands[0].pubkeys[0]) } @Test - fun craftSimpleSpendWithParties() { + fun generateSimpleSpendWithParties() { val tx = TransactionBuilder() - Cash().craftSpend(tx, 80.DOLLARS, ALICE, WALLET, setOf(MINI_CORP)) + Cash().generateSpend(tx, 80.DOLLARS, ALICE, WALLET, setOf(MINI_CORP)) assertEquals(WALLET[2].ref, tx.inputStates()[0]) } @Test - fun craftSimpleSpendWithChange() { + fun generateSimpleSpendWithChange() { val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputStates[0]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputStates[0]) - assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputStates[1]) + assertEquals(WALLET[0].ref, wtx.inputs[0]) + assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputs[0]) + assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputs[1]) assertEquals(OUR_PUBKEY_1, wtx.commands[0].pubkeys[0]) } @Test - fun craftSpendWithTwoInputs() { + fun generateSpendWithTwoInputs() { val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputStates[0]) - assertEquals(WALLET[1].ref, wtx.inputStates[1]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputStates[0]) + assertEquals(WALLET[0].ref, wtx.inputs[0]) + assertEquals(WALLET[1].ref, wtx.inputs[1]) + assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0]) assertEquals(OUR_PUBKEY_1, wtx.commands[0].pubkeys[0]) } @Test - fun craftSpendMixedDeposits() { + fun generateSpendMixedDeposits() { val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputStates[0]) - assertEquals(WALLET[1].ref, wtx.inputStates[1]) - assertEquals(WALLET[2].ref, wtx.inputStates[2]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputStates[0]) - assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputStates[1]) + assertEquals(WALLET[0].ref, wtx.inputs[0]) + assertEquals(WALLET[1].ref, wtx.inputs[1]) + assertEquals(WALLET[2].ref, wtx.inputs[2]) + assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0]) + assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1]) assertEquals(OUR_PUBKEY_1, wtx.commands[0].pubkeys[0]) } @Test - fun craftSpendInsufficientBalance() { + fun generateSpendInsufficientBalance() { val e: InsufficientBalanceException = assertFailsWith("balance") { makeSpend(1000.DOLLARS, THEIR_PUBKEY_1) } diff --git a/src/test/kotlin/contracts/CommercialPaperTests.kt b/src/test/kotlin/contracts/CommercialPaperTests.kt index 14fabd67bd..40b0df4fe7 100644 --- a/src/test/kotlin/contracts/CommercialPaperTests.kt +++ b/src/test/kotlin/contracts/CommercialPaperTests.kt @@ -9,6 +9,7 @@ package contracts import core.* +import core.crypto.SecureHash import core.node.TimestampingError import core.testutils.* import org.junit.Test @@ -115,7 +116,7 @@ class CommercialPaperTestsGeneric { fun `timestamp out of range`() { // Check what happens if the timestamp on the transaction itself defines a range that doesn't include true // time as measured by a TSA (which is running five hours ahead in this test). - CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply { + CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply { setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) signWith(MINI_CORP_KEY) assertFailsWith(TimestampingError.NotOnTimeException::class) { @@ -123,7 +124,7 @@ class CommercialPaperTestsGeneric { } } // Check that it also fails if true time is before the threshold (we are trying to timestamp too early). - CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply { + CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply { setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) signWith(MINI_CORP_KEY) assertFailsWith(TimestampingError.NotOnTimeException::class) { @@ -162,14 +163,14 @@ class CommercialPaperTestsGeneric { fun cashOutputsToWallet(vararg states: Cash.State): Pair>> { val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256()) - return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, ContractStateRef(ltx.hash, index)) }) + return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.hash, index)) }) } @Test fun `issue move and then redeem`() { // MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. val issueTX: LedgerTransaction = run { - val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply { + val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply { setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) signWith(MINI_CORP_KEY) timestamp(DUMMY_TIMESTAMPER) @@ -187,8 +188,8 @@ class CommercialPaperTestsGeneric { // Alice pays $9000 to MiniCorp to own some of their debt. val moveTX: LedgerTransaction = run { val ptx = TransactionBuilder() - Cash().craftSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet) - CommercialPaper().craftMove(ptx, issueTX.outRef(0), ALICE) + Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet) + CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE) ptx.signWith(MINI_CORP_KEY) ptx.signWith(ALICE_KEY) val stx = ptx.toSignedTransaction() @@ -204,7 +205,7 @@ class CommercialPaperTestsGeneric { fun makeRedeemTX(time: Instant): LedgerTransaction { val ptx = TransactionBuilder() ptx.setTime(time, DummyTimestampingAuthority.identity, 30.seconds) - CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet) + CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet) ptx.signWith(ALICE_KEY) ptx.signWith(MINI_CORP_KEY) ptx.timestamp(DUMMY_TIMESTAMPER) @@ -215,11 +216,11 @@ class CommercialPaperTestsGeneric { val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days) val e = assertFailsWith(TransactionVerificationException::class) { - TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(corpWalletTX, alicesWalletTX)).verify(TEST_PROGRAM_MAP) + TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(corpWalletTX, alicesWalletTX)).verify(MockContractFactory) } assertTrue(e.cause!!.message!!.contains("paper must have matured")) - TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify(TEST_PROGRAM_MAP) + TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify(MockContractFactory) } // Generate a trade lifecycle with various parameters. diff --git a/src/test/kotlin/contracts/CrowdFundTests.kt b/src/test/kotlin/contracts/CrowdFundTests.kt index 8880e141a5..d8292537b6 100644 --- a/src/test/kotlin/contracts/CrowdFundTests.kt +++ b/src/test/kotlin/contracts/CrowdFundTests.kt @@ -9,6 +9,7 @@ package contracts import core.* +import core.crypto.SecureHash import core.testutils.* import org.junit.Test import java.time.Instant @@ -99,7 +100,7 @@ class CrowdFundTests { fun cashOutputsToWallet(vararg states: Cash.State): Pair>> { val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256()) - return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, ContractStateRef(ltx.hash, index)) }) + return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.hash, index)) }) } @Test @@ -107,7 +108,7 @@ class CrowdFundTests { // MiniCorp registers a crowdfunding of $1,000, to close in 7 days. val registerTX: LedgerTransaction = run { // craftRegister returns a partial transaction - val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days).apply { + val ptx = CrowdFund().generateRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days).apply { setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) signWith(MINI_CORP_KEY) timestamp(DUMMY_TIMESTAMPER) @@ -126,8 +127,8 @@ class CrowdFundTests { // Alice pays $1000 to MiniCorp to fund their campaign. val pledgeTX: LedgerTransaction = run { val ptx = TransactionBuilder() - CrowdFund().craftPledge(ptx, registerTX.outRef(0), ALICE) - Cash().craftSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet) + CrowdFund().generatePledge(ptx, registerTX.outRef(0), ALICE) + Cash().generateSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet) ptx.setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) ptx.signWith(ALICE_KEY) ptx.timestamp(DUMMY_TIMESTAMPER) @@ -145,7 +146,7 @@ class CrowdFundTests { fun makeFundedTX(time: Instant): LedgerTransaction { val ptx = TransactionBuilder() ptx.setTime(time, DUMMY_TIMESTAMPER.identity, 30.seconds) - CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet) + CrowdFund().generateClose(ptx, pledgeTX.outRef(0), miniCorpWallet) ptx.signWith(MINI_CORP_KEY) ptx.timestamp(DUMMY_TIMESTAMPER) val stx = ptx.toSignedTransaction() @@ -156,11 +157,11 @@ class CrowdFundTests { val validClose = makeFundedTX(TEST_TX_TIME + 8.days) val e = assertFailsWith(TransactionVerificationException::class) { - TransactionGroup(setOf(registerTX, pledgeTX, tooEarlyClose), setOf(miniCorpWalletTx, aliceWalletTX)).verify(TEST_PROGRAM_MAP) + TransactionGroup(setOf(registerTX, pledgeTX, tooEarlyClose), setOf(miniCorpWalletTx, aliceWalletTX)).verify(MockContractFactory) } assertTrue(e.cause!!.message!!.contains("the closing date has past")) // This verification passes - TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(TEST_PROGRAM_MAP) + TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(MockContractFactory) } } \ No newline at end of file diff --git a/src/test/kotlin/core/MockServices.kt b/src/test/kotlin/core/MockServices.kt index ec31ad3b4d..18854cd2dd 100644 --- a/src/test/kotlin/core/MockServices.kt +++ b/src/test/kotlin/core/MockServices.kt @@ -8,6 +8,10 @@ package core +import core.crypto.DigitalSignature +import core.crypto.SecureHash +import core.crypto.generateKeyPair +import core.crypto.signWithECDSA import core.messaging.MessagingService import core.messaging.MockNetworkMap import core.messaging.NetworkMap @@ -15,9 +19,9 @@ import core.node.TimestampingError import core.serialization.SerializedBytes import core.serialization.deserialize import core.testutils.TEST_KEYS_TO_CORP_MAP +import core.testutils.TEST_PROGRAM_MAP import core.testutils.TEST_TX_TIME import java.security.KeyPair -import java.security.KeyPairGenerator import java.security.PrivateKey import java.security.PublicKey import java.time.Clock @@ -51,7 +55,7 @@ object MockIdentityService : IdentityService { class MockKeyManagementService( override val keys: Map, - val nextKeys: MutableList = arrayListOf(KeyPairGenerator.getInstance("EC").genKeyPair()) + val nextKeys: MutableList = arrayListOf(generateKeyPair()) ) : KeyManagementService { override fun freshKey() = nextKeys.removeAt(nextKeys.lastIndex) } @@ -62,7 +66,7 @@ class MockWalletService(val states: List>) : WalletSer @ThreadSafe class MockStorageService : StorageService { - override val myLegalIdentityKey: KeyPair = KeyPairGenerator.getInstance("EC").genKeyPair() + override val myLegalIdentityKey: KeyPair = generateKeyPair() override val myLegalIdentity: Party = Party("Unit test party", myLegalIdentityKey.public) private val tables = HashMap>() @@ -75,6 +79,14 @@ class MockStorageService : StorageService { } } +object MockContractFactory : ContractFactory { + override operator fun get(hash: SecureHash): T { + val clazz = TEST_PROGRAM_MAP[hash] ?: throw UnknownContractException() + @Suppress("UNCHECKED_CAST") + return clazz.newInstance() as T + } +} + class MockServices( val wallet: WalletService? = null, val keyManagement: KeyManagementService? = null, diff --git a/src/test/kotlin/core/TransactionGroupTests.kt b/src/test/kotlin/core/TransactionGroupTests.kt index dd3575c37a..cfbfeb8502 100644 --- a/src/test/kotlin/core/TransactionGroupTests.kt +++ b/src/test/kotlin/core/TransactionGroupTests.kt @@ -9,6 +9,7 @@ package core import contracts.Cash +import core.crypto.SecureHash import core.testutils.* import org.junit.Test import kotlin.test.assertEquals @@ -73,7 +74,7 @@ class TransactionGroupTests { val e = assertFailsWith(TransactionConflictException::class) { verify() } - assertEquals(ContractStateRef(t.hash, 0), e.conflictRef) + assertEquals(StateRef(t.hash, 0), e.conflictRef) assertEquals(setOf(conflict1, conflict2), setOf(e.tx1, e.tx2)) } } @@ -95,7 +96,7 @@ 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 = ContractStateRef(SecureHash.randomSHA256(), 0) + 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()) ) diff --git a/src/test/kotlin/core/messaging/InMemoryMessagingTests.kt b/src/test/kotlin/core/messaging/InMemoryMessagingTests.kt index 5d4b523095..e6cdc5ae43 100644 --- a/src/test/kotlin/core/messaging/InMemoryMessagingTests.kt +++ b/src/test/kotlin/core/messaging/InMemoryMessagingTests.kt @@ -21,10 +21,10 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue open class TestWithInMemoryNetwork { - val nodes: MutableMap = HashMap() + val nodes: MutableMap = HashMap() lateinit var network: InMemoryNetwork - fun makeNode(inBackground: Boolean = false): Pair { + fun makeNode(inBackground: Boolean = false): Pair { // The manuallyPumped = true bit means that we must call the pump method on the system in order to val (address, builder) = network.createNode(!inBackground) val node = builder.start().get() diff --git a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt index 26ba415f25..88f3b96220 100644 --- a/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt +++ b/src/test/kotlin/core/messaging/TwoPartyTradeProtocolTests.kt @@ -14,14 +14,12 @@ import contracts.CommercialPaper import contracts.protocols.TwoPartyTradeProtocol import core.* import core.testutils.* +import core.utilities.BriefLogFormatter import org.junit.After import org.junit.Before import org.junit.Test +import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -import java.util.logging.Formatter -import java.util.logging.Level -import java.util.logging.LogRecord -import java.util.logging.Logger import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -32,24 +30,21 @@ import kotlin.test.assertTrue * We assume that Alice and Bob already found each other via some market, and have agreed the details already. */ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() { + lateinit var backgroundThread: ExecutorService + @Before - fun initLogging() { - Logger.getLogger("").handlers[0].level = Level.ALL - Logger.getLogger("").handlers[0].formatter = object : Formatter() { - override fun format(record: LogRecord) = "${record.threadID} ${record.loggerName}: ${record.message}\n" - } - Logger.getLogger("com.r3cev.protocols.trade").level = Level.ALL + fun before() { + backgroundThread = Executors.newSingleThreadExecutor() + BriefLogFormatter.initVerbose("platform.trade") } @After - fun stopLogging() { - Logger.getLogger("com.r3cev.protocols.trade").level = Level.INFO + fun after() { + backgroundThread.shutdown() } @Test fun cashForCP() { - val backgroundThread = Executors.newSingleThreadExecutor() - transactionGroupFor { // Bob (Buyer) has some cash, Alice (Seller) has some commercial paper she wants to sell to Bob. roots { @@ -96,7 +91,6 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() { txns.add(aliceResult.get().second) verify() } - backgroundThread.shutdown() } @Test @@ -127,6 +121,10 @@ class TwoPartyTradeProtocolTests : TestWithInMemoryNetwork() { val smmBuyer = StateMachineManager(bobsServices, MoreExecutors.directExecutor()) + // Horrible Gradle/Kryo/Quasar FUBAR workaround: just skip these tests when run under Gradle for now. + if (!smmBuyer.checkpointing) + return + val buyerSessionID = random63BitValue() TwoPartyTradeProtocol.runSeller( diff --git a/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt b/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt index 8204763a8f..42a08dbae8 100644 --- a/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt +++ b/src/test/kotlin/core/node/TimestamperNodeServiceTest.kt @@ -10,6 +10,7 @@ package core.node import co.paralleluniverse.fibers.Suspendable import core.* +import core.crypto.SecureHash import core.messaging.* import core.serialization.serialize import core.testutils.ALICE @@ -26,12 +27,12 @@ import kotlin.test.assertFailsWith import kotlin.test.assertTrue class TimestamperNodeServiceTest : TestWithInMemoryNetwork() { - lateinit var myNode: Pair - lateinit var serviceNode: Pair + lateinit var myNode: Pair + lateinit var serviceNode: Pair lateinit var service: TimestamperNodeService val ptx = TransactionBuilder().apply { - addInputState(ContractStateRef(SecureHash.randomSHA256(), 0)) + addInputState(StateRef(SecureHash.randomSHA256(), 0)) addOutputState(100.DOLLARS.CASH) } @@ -62,7 +63,7 @@ class TimestamperNodeServiceTest : TestWithInMemoryNetwork() { override fun call(): Boolean { val client = TimestamperClient(this, server) val ptx = TransactionBuilder().apply { - addInputState(ContractStateRef(SecureHash.randomSHA256(), 0)) + addInputState(StateRef(SecureHash.randomSHA256(), 0)) addOutputState(100.DOLLARS.CASH) } ptx.addCommand(TimestampCommand(now - 20.seconds, now + 20.seconds), server.identity.owningKey) diff --git a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt index fc95173c94..58179a2e8e 100644 --- a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt +++ b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt @@ -10,7 +10,11 @@ package core.serialization import contracts.Cash import core.* -import core.testutils.* +import core.crypto.SecureHash +import core.testutils.DUMMY_PUBKEY_1 +import core.testutils.MINI_CORP +import core.testutils.TEST_TX_TIME +import core.testutils.TestUtils import org.junit.Before import org.junit.Test import java.security.SignatureException @@ -24,7 +28,7 @@ class TransactionSerializationTests { val outputState = Cash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1) val changeState = Cash.State(depositRef, 400.POUNDS, TestUtils.keypair.public) - val fakeStateRef = ContractStateRef(SecureHash.sha256("fake tx id"), 0) + val fakeStateRef = StateRef(SecureHash.sha256("fake tx id"), 0) lateinit var tx: TransactionBuilder @Before @@ -91,8 +95,8 @@ class TransactionSerializationTests { val stx = tx.toSignedTransaction() val ltx = stx.verifyToLedgerTransaction(MockIdentityService) assertEquals(tx.commands().map { it.data }, ltx.commands.map { it.value }) - assertEquals(tx.inputStates(), ltx.inStateRefs) - assertEquals(tx.outputStates(), ltx.outStates) + assertEquals(tx.inputStates(), ltx.inputs) + assertEquals(tx.outputStates(), ltx.outputs) assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_TIMESTAMPER.identity)!!.midpoint) } } \ No newline at end of file diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index 37219b93ec..1dccf00ef3 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -12,8 +12,11 @@ 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.visualiser.GraphVisualiser -import java.security.KeyPairGenerator import java.security.PublicKey import java.time.Instant import java.util.* @@ -22,8 +25,8 @@ import kotlin.test.assertFailsWith import kotlin.test.fail object TestUtils { - val keypair = KeyPairGenerator.getInstance("EC").genKeyPair() - val keypair2 = KeyPairGenerator.getInstance("EC").genKeyPair() + val keypair = generateKeyPair() + val keypair2 = generateKeyPair() } // A few dummy values for testing. @@ -33,9 +36,9 @@ val MINI_CORP_KEY = TestUtils.keypair2 val MINI_CORP_PUBKEY = MINI_CORP_KEY.public val DUMMY_PUBKEY_1 = DummyPublicKey("x1") val DUMMY_PUBKEY_2 = DummyPublicKey("x2") -val ALICE_KEY = KeyPairGenerator.getInstance("EC").genKeyPair() +val ALICE_KEY = generateKeyPair() val ALICE = ALICE_KEY.public -val BOB_KEY = KeyPairGenerator.getInstance("EC").genKeyPair() +val BOB_KEY = generateKeyPair() val BOB = BOB_KEY.public val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY) val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY) @@ -49,13 +52,13 @@ val TEST_KEYS_TO_CORP_MAP: Map = mapOf( val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z") // In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside -// a sandbox. For now we just instantiate right at the start of the program. -val TEST_PROGRAM_MAP: Map = mapOf( - CASH_PROGRAM_ID to Cash(), - CP_PROGRAM_ID to CommercialPaper(), - JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper(), - CROWDFUND_PROGRAM_ID to CrowdFund(), - DUMMY_PROGRAM_ID to DummyContract +// a sandbox. For unit tests we just have a hard-coded list. +val TEST_PROGRAM_MAP: Map> = mapOf( + CASH_PROGRAM_ID to Cash::class.java, + CP_PROGRAM_ID to CommercialPaper::class.java, + JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper::class.java, + CROWDFUND_PROGRAM_ID to CrowdFund::class.java, + DUMMY_PROGRAM_ID to DummyContract::class.java ) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -79,7 +82,9 @@ val TEST_PROGRAM_MAP: Map = mapOf( // // TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block -infix fun Cash.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) +infix fun Cash.State.`owned by`(owner: PublicKey) = copy(owner = owner) +infix fun Cash.State.`issued by`(party: Party) = copy(deposit = deposit.copy(party = party)) +infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner) // Allows you to write 100.DOLLARS.CASH @@ -130,7 +135,7 @@ open class TransactionForTest : AbstractTransactionForTest() { protected fun run(time: Instant) { val cmds = commandsToAuthenticatedObjects() val tx = TransactionForVerification(inStates, outStates.map { it.state }, cmds, SecureHash.randomSHA256()) - tx.verify(TEST_PROGRAM_MAP) + tx.verify(MockContractFactory) } fun accepts(time: Instant = TEST_TX_TIME) = run(time) @@ -201,7 +206,7 @@ fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().appl class TransactionGroupDSL(private val stateType: Class) { open inner class LedgerTransactionDSL : AbstractTransactionForTest() { - private val inStates = ArrayList() + private val inStates = ArrayList() fun input(label: String) { inStates.add(label.outputRef) @@ -219,7 +224,7 @@ class TransactionGroupDSL(private val stateType: Class) { } val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found") - val String.outputRef: ContractStateRef get() = labelToRefs[this] ?: throw IllegalArgumentException("Unknown label \"$this\"") + val String.outputRef: StateRef get() = labelToRefs[this] ?: throw IllegalArgumentException("Unknown label \"$this\"") fun lookup(label: String) = StateAndRef(label.output as C, label.outputRef) @@ -228,7 +233,7 @@ class TransactionGroupDSL(private val stateType: Class) { val ltx = toLedgerTransaction() for ((index, labelledState) in outStates.withIndex()) { if (labelledState.label != null) { - labelToRefs[labelledState.label] = ContractStateRef(ltx.hash, index) + labelToRefs[labelledState.label] = StateRef(ltx.hash, index) if (stateType.isInstance(labelledState.state)) { labelToOutputs[labelledState.label] = labelledState.state as T } @@ -240,7 +245,7 @@ class TransactionGroupDSL(private val stateType: Class) { } private val rootTxns = ArrayList() - private val labelToRefs = HashMap() + private val labelToRefs = HashMap() private val labelToOutputs = HashMap() private val outputsToLabels = HashMap() @@ -253,7 +258,7 @@ class TransactionGroupDSL(private val stateType: Class) { val ltx = wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256()) for ((index, state) in outputStates.withIndex()) { val label = state.label!! - labelToRefs[label] = ContractStateRef(ltx.hash, index) + labelToRefs[label] = StateRef(ltx.hash, index) outputsToLabels[state.state] = label labelToOutputs[label] = state.state as T } @@ -292,7 +297,7 @@ class TransactionGroupDSL(private val stateType: Class) { fun verify() { val group = toTransactionGroup() try { - group.verify(TEST_PROGRAM_MAP) + 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 }!! @@ -317,4 +322,4 @@ class TransactionGroupDSL(private val stateType: Class) { } inline fun transactionGroupFor(body: TransactionGroupDSL.() -> Unit) = TransactionGroupDSL(T::class.java).apply { this.body() } -fun transactionGroup(body: TransactionGroupDSL.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() } \ No newline at end of file +fun transactionGroup(body: TransactionGroupDSL.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() } diff --git a/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt b/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt index 7a3149499d..3cdd05d274 100644 --- a/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt +++ b/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt @@ -10,7 +10,7 @@ package core.visualiser import core.CommandData import core.ContractState -import core.SecureHash +import core.crypto.SecureHash import core.testutils.TransactionGroupDSL import org.graphstream.graph.Edge import org.graphstream.graph.Node @@ -34,9 +34,9 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { txNode.styleClass = "tx" // Now create a vertex for each output state. - for (outIndex in tx.outStates.indices) { + for (outIndex in tx.outputs.indices) { val node = graph.addNode(tx.outRef(outIndex).ref.toString()) - val state = tx.outStates[outIndex] + val state = tx.outputs[outIndex] node.label = stateToLabel(state) node.styleClass = stateToCSSClass(state) + ",state" node.setAttribute("state", state) @@ -55,7 +55,7 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { } // And now all states and transactions were mapped to graph nodes, hook up the input edges. for ((txIndex, tx) in tg.transactions.withIndex()) { - for ((inputIndex, ref) in tx.inStateRefs.withIndex()) { + for ((inputIndex, ref) in tx.inputs.withIndex()) { val edge = graph.addEdge("tx$txIndex-in$inputIndex", ref.toString(), "tx$txIndex", true) edge.weight = 1.2 }