Merged in PLT-60-misc-code-cleanups (pull request #17)

Misc code cleanups
This commit is contained in:
Mike Hearn 2016-02-10 17:59:53 +01:00
commit 48192d8d9d
35 changed files with 699 additions and 483 deletions

4
.gitignore vendored
View File

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

View File

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

View File

@ -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<State>, newOwner: PublicKey) {
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, 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<CommercialPaper.State>`` 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<State>, wallet: List<StateAndRef<Cash.State>>) {
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
// 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.

View File

@ -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<State> paper, List<StateAndRef<Cash.State>> 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()));
}

View File

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

View File

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

View File

@ -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<StateAndRef<Cash.State>>, onlyFromParties: Set<Party>? = null): List<PublicKey> {
fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey,
cashStates: List<StateAndRef<Cash.State>>, onlyFromParties: Set<Party>? = null): List<PublicKey> {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.

View File

@ -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<State>, newOwner: PublicKey) {
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, 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<State>, wallet: List<StateAndRef<Cash.State>>) {
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
// 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)
}

View File

@ -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<State>, subscriber: PublicKey) {
fun generatePledge(tx: TransactionBuilder, campaign: StateAndRef<State>, 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<State>, wallet: List<StateAndRef<Cash.State>>) {
fun generateClose(tx: TransactionBuilder, campaign: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
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)
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<out T : ContractState>(val state: T, val ref: ContractStateRef)
data class StateAndRef<out T : ContractState>(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 <T : Contract> get(hash: SecureHash): T
}
class UnknownContractException : Exception()

View File

@ -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<LedgerTransaction>, val nonVerified
/**
* Verifies the group and returns the set of resolved transactions.
*/
fun verify(programMap: Map<SecureHash, Contract>): Set<TransactionForVerification> {
fun verify(programMap: ContractFactory): Set<TransactionForVerification> {
// 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<SecureHash, List<LedgerTransaction>> = (transactions + nonVerifiedRoots).groupBy { it.hash }
val refToConsumingTXMap = hashMapOf<ContractStateRef, LedgerTransaction>()
val refToConsumingTXMap = hashMapOf<StateRef, LedgerTransaction>()
val resolved = HashSet<TransactionForVerification>(transactions.size)
for (tx in transactions) {
val inputs = ArrayList<ContractState>(tx.inStateRefs.size)
for (ref in tx.inStateRefs) {
val inputs = ArrayList<ContractState>(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<LedgerTransaction>, 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<ContractState>,
* @throws IllegalStateException if a state refers to an unknown contract.
*/
@Throws(TransactionVerificationException::class, IllegalStateException::class)
fun verify(programMap: Map<SecureHash, Contract>) {
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) {

View File

@ -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<ContractStateRef>,
val outputStates: List<ContractState>,
data class WireTransaction(val inputs: List<StateRef>,
val outputs: List<ContractState>,
val commands: List<Command>) {
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<ContractStateRef>,
data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, val sigs: List<DigitalSignature.WithKey>) {
init { check(sigs.isNotEmpty()) }
// Lazily calculated access to the deserialised/hashed transaction data.
/** Lazily calculated access to the deserialised/hashed transaction data. */
val tx: WireTransaction by lazy { txBits.deserialize() }
/** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */
@ -121,11 +124,14 @@ data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, v
/** Returns the same transaction but with an additional (unchecked) signature */
fun withAdditionalSignature(sig: DigitalSignature.WithKey) = copy(sigs = sigs + sig)
/** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */
operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig)
}
/** A mutable transaction that's in the process of being built, before all signatures are present. */
class TransactionBuilder(private val inputStates: MutableList<ContractStateRef> = arrayListOf(),
private val outputStates: MutableList<ContractState> = arrayListOf(),
class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf(),
private val outputs: MutableList<ContractState> = arrayListOf(),
private val commands: MutableList<Command> = arrayListOf()) {
val time: TimestampCommand? get() = commands.mapNotNull { it.data as? TimestampCommand }.singleOrNull()
@ -152,8 +158,8 @@ class TransactionBuilder(private val inputStates: MutableList<ContractStateRef>
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<ContractStateRef>
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<ContractStateRef>
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<ContractStateRef>
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
// Accessors that yield immutable snapshots.
fun inputStates(): List<ContractStateRef> = ArrayList(inputStates)
fun outputStates(): List<ContractState> = ArrayList(outputStates)
fun inputStates(): List<StateRef> = ArrayList(inputs)
fun outputStates(): List<ContractState> = ArrayList(outputs)
fun commands(): List<Command> = ArrayList(commands)
}
@ -256,20 +262,20 @@ class TransactionBuilder(private val inputStates: MutableList<ContractStateRef>
* 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<ContractStateRef>,
/** The states that will be generated by the execution of this transaction. */
val outStates: List<ContractState>,
/** 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<StateRef>,
/** The states that will be generated by the execution of this transaction. */
val outputs: List<ContractState>,
/** Arbitrary data passed to the program of each input state. */
val commands: List<AuthenticatedObject<CommandData>>,
/** The hash of the original serialised SignedTransaction */
/** The hash of the original serialised SignedTransaction */
val hash: SecureHash
) {
@Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int) = StateAndRef(outStates[index] as T, ContractStateRef(hash, index))
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as T, StateRef(hash, index))
fun <T : ContractState> outRef(state: T): StateAndRef<T> {
val i = outStates.indexOf(state)
val i = outputs.indexOf(state)
if (i == -1)
throw IllegalArgumentException("State not found in this transaction")
return outRef(i)

View File

@ -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<Handle, Node>()
private val handleNodeMap = HashMap<Handle, InMemoryNode>()
// 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<Handle, LinkedBlockingQueue<Message>>()
val nodes: List<Node> @Synchronized get() = handleNodeMap.values.toList()
val nodes: List<InMemoryNode> @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<Handle, MessagingServiceBuilder<Node>> {
fun createNode(manuallyPumped: Boolean): Pair<Handle, MessagingServiceBuilder<InMemoryNode>> {
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<Node> {
fun createNodeWithID(manuallyPumped: Boolean, id: Int): MessagingServiceBuilder<InMemoryNode> {
return Builder(manuallyPumped, Handle(id))
}
@ -103,10 +103,10 @@ public class InMemoryNetwork {
messageQueues.clear()
}
inner class Builder(val manuallyPumped: Boolean, val id: Handle) : MessagingServiceBuilder<Node> {
override fun start(): ListenableFuture<Node> {
inner class Builder(val manuallyPumped: Boolean, val id: Handle) : MessagingServiceBuilder<InMemoryNode> {
override fun start(): ListenableFuture<InMemoryNode> {
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<LegallyIdentifiableNode, Node> {
fun setupTimestampingNode(manuallyPumped: Boolean): Pair<LegallyIdentifiableNode, InMemoryNode> {
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")

View File

@ -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<ProtocolStateMachine<*>>())
// 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<ProtocolStateMachine<*>> 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<R> : Fiber<R>("protocol", SameThreadFiberSch
@Transient protected lateinit var logger: Logger
@Transient private var _resultFuture: SettableFuture<R>? = SettableFuture.create<R>()
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<R> get() {
return _resultFuture ?: run {
@ -263,7 +272,7 @@ abstract class ProtocolStateMachine<R> : Fiber<R>("protocol", SameThreadFiberSch
}
@Suspendable @Suppress("UNCHECKED_CAST")
private fun <T : Any> suspendAndExpectReceive(with: FiberRequest): T {
private fun <T : Any> suspendAndExpectReceive(with: FiberRequest): UntrustworthyData<T> {
Fiber.parkAndSerialize { fiber, serializer ->
// We don't use the passed-in serializer here, because we need to use our own augmented Kryo.
val deserializer = Fiber.getFiberSerializer() as KryoSerializer
@ -276,18 +285,18 @@ abstract class ProtocolStateMachine<R> : Fiber<R>("protocol", SameThreadFiberSch
}
val tmp = resumeWithObject ?: throw IllegalStateException("Expected to receive something")
resumeWithObject = null
return tmp as T
return UntrustworthyData(tmp as T)
}
@Suspendable @Suppress("UNCHECKED_CAST")
fun <T : Any> sendAndReceive(topic: String, destination: MessageRecipients, sessionIDForSend: Long, sessionIDForReceive: Long,
obj: Any, recvType: Class<T>): T {
obj: Any, recvType: Class<T>): UntrustworthyData<T> {
val result = FiberRequest.ExpectingResponse(topic, destination, sessionIDForSend, sessionIDForReceive, obj, recvType)
return suspendAndExpectReceive(result)
}
@Suspendable
fun <T : Any> receive(topic: String, sessionIDForReceive: Long, recvType: Class<T>): T {
fun <T : Any> receive(topic: String, sessionIDForReceive: Long, recvType: Class<T>): UntrustworthyData<T> {
val result = FiberRequest.ExpectingResponse(topic, null, -1, sessionIDForReceive, null, recvType)
return suspendAndExpectReceive(result)
}
@ -299,6 +308,29 @@ abstract class ProtocolStateMachine<R> : Fiber<R>("protocol", SameThreadFiberSch
}
}
/**
* A small utility to approximate taint tracking: if a method gives you back one of these, it means the data came from
* a remote source that may be incentivised to pass us junk that violates basic assumptions and thus must be checked
* first. The wrapper helps you to avoid forgetting this vital step. Things you might want to check are:
*
* - Is this object the one you actually expected? Did the other side hand you back something technically valid but
* not what you asked for?
* - Is the object disobeying its own invariants?
* - Are any objects *reachable* from this object mismatched or not what you expected?
* - Is it suspiciously large or small?
*/
class UntrustworthyData<T>(private val fromUntrustedWorld: T) {
val data: T
@Deprecated("Accessing the untrustworthy data directly without validating it first is a bad idea")
get() = fromUntrustedWorld
@Suppress("DEPRECATION")
inline fun validate(validator: (T) -> Unit): T {
validator(data)
return data
}
}
// TODO: Clean this up
open class FiberRequest(val topic: String, val destination: MessageRecipients?,
val sessionIDForSend: Long, val sessionIDForReceive: Long, val obj: Any?) {

View File

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

View File

@ -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<PublicKey, PrivateKey> 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
}

View File

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

View File

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

View File

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

View File

@ -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<String>) {
@ -73,8 +58,7 @@ fun main(args: Array<String>) {
} 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<String>) {
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<Party>(includeClassName = true)
LegallyIdentifiableNode(ArtemisMessagingService.makeRecipient(addr), party)
@ -140,9 +124,9 @@ fun main(args: Array<String>) {
// 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<Long>()
@ -175,10 +159,10 @@ fun main(args: Array<String>) {
fun makeFakeCommercialPaper(ownedBy: PublicKey): StateAndRef<CommercialPaper.State> {
// 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."

View File

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

View File

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

View File

@ -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<LedgerTransaction, List<StateAndRef<Cash.State>>> {
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.

View File

@ -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<LedgerTransaction, List<StateAndRef<Cash.State>>> {
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)
}
}

View File

@ -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<PublicKey, PrivateKey>,
val nextKeys: MutableList<KeyPair> = arrayListOf(KeyPairGenerator.getInstance("EC").genKeyPair())
val nextKeys: MutableList<KeyPair> = arrayListOf(generateKeyPair())
) : KeyManagementService {
override fun freshKey() = nextKeys.removeAt(nextKeys.lastIndex)
}
@ -62,7 +66,7 @@ class MockWalletService(val states: List<StateAndRef<OwnableState>>) : 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<String, MutableMap<Any, Any>>()
@ -75,6 +79,14 @@ class MockStorageService : StorageService {
}
}
object MockContractFactory : ContractFactory {
override operator fun <T : Contract> 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,

View File

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

View File

@ -21,10 +21,10 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
open class TestWithInMemoryNetwork {
val nodes: MutableMap<InMemoryNetwork.Handle, InMemoryNetwork.Node> = HashMap()
val nodes: MutableMap<InMemoryNetwork.Handle, InMemoryNetwork.InMemoryNode> = HashMap()
lateinit var network: InMemoryNetwork
fun makeNode(inBackground: Boolean = false): Pair<InMemoryNetwork.Handle, InMemoryNetwork.Node> {
fun makeNode(inBackground: Boolean = false): Pair<InMemoryNetwork.Handle, InMemoryNetwork.InMemoryNode> {
// 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()

View File

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

View File

@ -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<InMemoryNetwork.Handle, InMemoryNetwork.Node>
lateinit var serviceNode: Pair<InMemoryNetwork.Handle, InMemoryNetwork.Node>
lateinit var myNode: Pair<InMemoryNetwork.Handle, InMemoryNetwork.InMemoryNode>
lateinit var serviceNode: Pair<InMemoryNetwork.Handle, InMemoryNetwork.InMemoryNode>
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)

View File

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

View File

@ -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<PublicKey, Party> = 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<SecureHash, Contract> = 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<SecureHash, Class<out Contract>> = 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<SecureHash, Contract> = 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<T : ContractState>(private val stateType: Class<T>) {
open inner class LedgerTransactionDSL : AbstractTransactionForTest() {
private val inStates = ArrayList<ContractStateRef>()
private val inStates = ArrayList<StateRef>()
fun input(label: String) {
inStates.add(label.outputRef)
@ -219,7 +224,7 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
}
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 <C : ContractState> lookup(label: String) = StateAndRef(label.output as C, label.outputRef)
@ -228,7 +233,7 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
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<T : ContractState>(private val stateType: Class<T>) {
}
private val rootTxns = ArrayList<LedgerTransaction>()
private val labelToRefs = HashMap<String, ContractStateRef>()
private val labelToRefs = HashMap<String, StateRef>()
private val labelToOutputs = HashMap<String, T>()
private val outputsToLabels = HashMap<ContractState, String>()
@ -253,7 +258,7 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
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<T : ContractState>(private val stateType: Class<T>) {
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<T : ContractState>(private val stateType: Class<T>) {
}
inline fun <reified T : ContractState> transactionGroupFor(body: TransactionGroupDSL<T>.() -> Unit) = TransactionGroupDSL<T>(T::class.java).apply { this.body() }
fun transactionGroup(body: TransactionGroupDSL<ContractState>.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() }
fun transactionGroup(body: TransactionGroupDSL<ContractState>.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() }

View File

@ -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<in ContractState>) {
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<Node>(tx.outRef<ContractState>(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<in ContractState>) {
}
// 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<Edge>("tx$txIndex-in$inputIndex", ref.toString(), "tx$txIndex", true)
edge.weight = 1.2
}