Merged tx-simplifications into master

This commit is contained in:
Mike Hearn 2015-12-22 15:07:51 +00:00
commit c217702606
19 changed files with 338 additions and 220 deletions

View File

@ -18,7 +18,7 @@ are created and destroyed by digitally signed **transactions**. Each transaction
consume/destroy, these are called **inputs**, and contains a set of new states that it will create, these are called consume/destroy, these are called **inputs**, and contains a set of new states that it will create, these are called
**outputs**. **outputs**.
States contain arbitrary data, but they always contain at minimum a pointer to the bytecode of a States contain arbitrary data, but they always contain at minimum a hash of the bytecode of a
**code contract**, which is a program expressed in some byte code that runs sandboxed inside a virtual machine. Code **code contract**, which is a program expressed in some byte code that runs sandboxed inside a virtual machine. Code
contracts (or just "contracts" in the rest of this document) are globally shared pieces of business logic. Contracts contracts (or just "contracts" in the rest of this document) are globally shared pieces of business logic. Contracts
define a **verify function**, which is a pure function given the entire transaction as input. define a **verify function**, which is a pure function given the entire transaction as input.
@ -26,26 +26,21 @@ define a **verify function**, which is a pure function given the entire transact
To be considered valid, the transaction must be **accepted** by the verify function of every contract pointed to by the To be considered valid, the transaction must be **accepted** by the verify function of every contract pointed to by the
input and output states. Beyond inputs and outputs, transactions may also contain **commands**, small data packets that input and output states. Beyond inputs and outputs, transactions may also contain **commands**, small data packets that
the platform does not interpret itself, but which can parameterise execution of the contracts. They can be thought of as the platform does not interpret itself, but which can parameterise execution of the contracts. They can be thought of as
arguments to the verify function. arguments to the verify function. Each command has a list of **public keys** associated with it. The platform ensures
that the transaction is signed by every key listed in the commands before the contracts start to execute. Public keys
may be random/identityless for privacy, or linked to a well known legal identity via a *public key infrastructure* (PKI).
Note that there is nothing that explicitly binds together specific inputs, outputs or commands. Instead it's up to the Note that there is nothing that explicitly binds together specific inputs, outputs or commands. Instead it's up to the
contract code to interpret the pieces inside the transaction and ensure they fit together correctly. This is done to contract code to interpret the pieces inside the transaction and ensure they fit together correctly. This is done to
maximise flexibility for the contract developer. maximise flexibility for the contract developer.
A transaction has one or more **signatures** attached to it. The signatures do not mean anything by themselves, rather, Transactions may sometimes need to provide a contract with data from the outside world. Examples may include stock
their existence is given as input to the contract which can then decide which set of signatures it demands (if any). prices, facts about events or the statuses of legal entities (e.g. bankruptcy), and so on. The providers of such
Signatures may be from an arbitrary, random **public key** that has no identity attached. A public key may be facts are called **oracles** and they provide facts to the ledger by signing transactions that contain commands they
well known, that is, appears in some sort of public identity registry. In this case we say the key is owned by a recognise. The commands contain the fact and the signature shows agreement to that fact. Time is also modelled as
**party**, which is defined (for now) as being merely a (public key, name) pair. a fact, with the signature of a special kind of oracle called a **timestamping authority** (TSA). A TSA signs
a transaction if a pre-defined timestamping command in it defines a after/before time window that includes "true
A transaction may also be **timestamped**. A timestamp is a (hash, datetime, signature) triple from a time" (i.e. GPS time as calibrated to the US Naval Observatory).
*timestamping authority* (TSA). The notion of a TSA is not ledger specific and is defined by
`IETF RFC 3161 <https://www.ietf.org/rfc/rfc3161.txt>`_ which defines the internet standard Timestamping Protocol (TSP).
The purpose of the TSA is to attach a single, globally agreed upon time which a contract may use to enforce certain
types of time-based logic. The TSA's do not need to know about the contents of the transaction in order to provide a
timestamp, and they are therefore never exposed to private data.
.. note:: In the current code, use of TSAs is not implemented.
As the same terminology often crops up in different distributed ledger designs, let's compare this to other As the same terminology often crops up in different distributed ledger designs, let's compare this to other
distributed ledger systems you may be familiar with. You can find more detailed design rationales for why the platform distributed ledger systems you may be familiar with. You can find more detailed design rationales for why the platform

View File

@ -66,8 +66,8 @@ class Cash : Contract {
} }
// Just for grouping // Just for grouping
interface Commands : Command { interface Commands : CommandData {
class Move() : TypeOnlyCommand(), Commands class Move() : TypeOnlyCommandData(), Commands
/** /**
* Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction * Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
@ -155,7 +155,7 @@ class Cash : Contract {
check(tx.inputStates().isEmpty()) check(tx.inputStates().isEmpty())
check(tx.outputStates().sumCashOrNull() == null) check(tx.outputStates().sumCashOrNull() == null)
tx.addOutputState(Cash.State(at, amount, owner)) tx.addOutputState(Cash.State(at, amount, owner))
tx.addArg(WireCommand(Cash.Commands.Issue(), listOf(at.party.owningKey))) tx.addCommand(Cash.Commands.Issue(), at.party.owningKey)
} }
/** /**
@ -233,7 +233,7 @@ class Cash : Contract {
for (state in outputs) tx.addOutputState(state) for (state in outputs) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code? // What if we already have a move command with the right keys? Filter it out here or in platform code?
val keysList = keysUsed.toList() val keysList = keysUsed.toList()
tx.addArg(WireCommand(Commands.Move(), keysList)) tx.addCommand(Commands.Move(), keysList)
return keysList return keysList
} }
} }

View File

@ -53,12 +53,12 @@ class CommercialPaper : Contract {
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
} }
interface Commands : Command { interface Commands : CommandData {
class Move : TypeOnlyCommand(), Commands class Move : TypeOnlyCommandData(), Commands
class Redeem : TypeOnlyCommand(), Commands class Redeem : TypeOnlyCommandData(), Commands
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP. // We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
// However, nothing in the platform enforces that uniqueness: it's up to the issuer. // However, nothing in the platform enforces that uniqueness: it's up to the issuer.
class Issue : TypeOnlyCommand(), Commands class Issue : TypeOnlyCommandData(), Commands
} }
override fun verify(tx: TransactionForVerification) { override fun verify(tx: TransactionForVerification) {
@ -68,7 +68,7 @@ class CommercialPaper : Contract {
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
// it for cash on or after the maturity date. // it for cash on or after the maturity date.
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>() val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
val time = tx.time val timestamp: TimestampCommand? = tx.getTimestampBy(DummyTimestampingAuthority.identity)
for (group in groups) { for (group in groups) {
when (command.value) { when (command.value) {
@ -83,7 +83,7 @@ class CommercialPaper : Contract {
is Commands.Redeem -> { is Commands.Redeem -> {
val input = group.inputs.single() val input = group.inputs.single()
val received = tx.outStates.sumCashBy(input.owner) val received = tx.outStates.sumCashBy(input.owner)
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped") val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
requireThat { requireThat {
"the paper must have matured" by (time > input.maturityDate) "the paper must have matured" by (time > input.maturityDate)
"the received amount equals the face value" by (received == input.faceValue) "the received amount equals the face value" by (received == input.faceValue)
@ -94,7 +94,7 @@ class CommercialPaper : Contract {
is Commands.Issue -> { is Commands.Issue -> {
val output = group.outputs.single() val output = group.outputs.single()
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped") val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
requireThat { requireThat {
// Don't allow people to issue commercial paper under other entities identities. // Don't allow people to issue commercial paper under other entities identities.
"the issuance is signed by the claimed issuer of the paper" by "the issuance is signed by the claimed issuer of the paper" by
@ -119,7 +119,7 @@ class CommercialPaper : Contract {
*/ */
fun craftIssue(issuance: PartyReference, faceValue: Amount, maturityDate: Instant): PartialTransaction { fun craftIssue(issuance: PartyReference, faceValue: Amount, maturityDate: Instant): PartialTransaction {
val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate) val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate)
return PartialTransaction().withItems(state, WireCommand(Commands.Issue(), issuance.party.owningKey)) return PartialTransaction().withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
} }
/** /**
@ -128,7 +128,7 @@ class CommercialPaper : Contract {
fun craftMove(tx: PartialTransaction, paper: StateAndRef<State>, newOwner: PublicKey) { fun craftMove(tx: PartialTransaction, paper: StateAndRef<State>, newOwner: PublicKey) {
tx.addInputState(paper.ref) tx.addInputState(paper.ref)
tx.addOutputState(paper.state.copy(owner = newOwner)) tx.addOutputState(paper.state.copy(owner = newOwner))
tx.addArg(WireCommand(Commands.Move(), paper.state.owner)) tx.addCommand(Commands.Move(), paper.state.owner)
} }
/** /**
@ -143,7 +143,7 @@ class CommercialPaper : Contract {
// Add the cash movement using the states in our wallet. // Add the cash movement using the states in our wallet.
Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet) Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet)
tx.addInputState(paper.ref) tx.addInputState(paper.ref)
tx.addArg(WireCommand(CommercialPaper.Commands.Redeem(), paper.state.owner)) tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner)
} }
} }

View File

@ -66,10 +66,10 @@ class CrowdFund : Contract {
) )
interface Commands : Command { interface Commands : CommandData {
class Register : TypeOnlyCommand(), Commands class Register : TypeOnlyCommandData(), Commands
class Pledge : TypeOnlyCommand(), Commands class Pledge : TypeOnlyCommandData(), Commands
class Close : TypeOnlyCommand(), Commands class Close : TypeOnlyCommandData(), Commands
} }
override fun verify(tx: TransactionForVerification) { override fun verify(tx: TransactionForVerification) {
@ -79,7 +79,9 @@ class CrowdFund : Contract {
val command = tx.commands.requireSingleCommand<CrowdFund.Commands>() val command = tx.commands.requireSingleCommand<CrowdFund.Commands>()
val outputCrowdFund: CrowdFund.State = tx.outStates.filterIsInstance<CrowdFund.State>().single() val outputCrowdFund: CrowdFund.State = tx.outStates.filterIsInstance<CrowdFund.State>().single()
val outputCash: List<Cash.State> = tx.outStates.filterIsInstance<Cash.State>() val outputCash: List<Cash.State> = tx.outStates.filterIsInstance<Cash.State>()
val time = tx.time
val time = tx.getTimestampBy(DummyTimestampingAuthority.identity)?.midpoint
if (time == null) throw IllegalArgumentException("must be timestamped")
when (command.value) { when (command.value) {
is Commands.Register -> { is Commands.Register -> {
@ -89,7 +91,7 @@ class CrowdFund : Contract {
"the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty()) "the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty())
"the output registration has a non-zero target" by (outputCrowdFund.campaign.target.pennies > 0) "the output registration has a non-zero target" by (outputCrowdFund.campaign.target.pennies > 0)
"the output registration has a name" by (outputCrowdFund.campaign.name.isNotBlank()) "the output registration has a name" by (outputCrowdFund.campaign.name.isNotBlank())
"the output registration has a closing time in the future" by (outputCrowdFund.campaign.closingTime > tx.time) "the output registration has a closing time in the future" by (time < outputCrowdFund.campaign.closingTime)
"the output registration has an open state" by (!outputCrowdFund.closed) "the output registration has an open state" by (!outputCrowdFund.closed)
} }
} }
@ -97,7 +99,6 @@ class CrowdFund : Contract {
is Commands.Pledge -> { is Commands.Pledge -> {
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single() val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
val pledgedCash = outputCash.sumCashBy(inputCrowdFund.campaign.owner) val pledgedCash = outputCash.sumCashBy(inputCrowdFund.campaign.owner)
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
requireThat { requireThat {
"campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign) "campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign)
"the campaign is still open" by (inputCrowdFund.campaign.closingTime >= time) "the campaign is still open" by (inputCrowdFund.campaign.closingTime >= time)
@ -117,8 +118,6 @@ class CrowdFund : Contract {
return true return true
} }
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
requireThat { requireThat {
"campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign) "campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign)
"the closing date has past" by (time >= outputCrowdFund.campaign.closingTime) "the closing date has past" by (time >= outputCrowdFund.campaign.closingTime)
@ -146,7 +145,7 @@ class CrowdFund : Contract {
fun craftRegister(owner: PartyReference, fundingTarget: Amount, fundingName: String, closingTime: Instant): PartialTransaction { fun craftRegister(owner: PartyReference, fundingTarget: Amount, fundingName: String, closingTime: Instant): PartialTransaction {
val campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime) val campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime)
val state = State(campaign) val state = State(campaign)
return PartialTransaction().withItems(state, WireCommand(Commands.Register(), owner.party.owningKey)) return PartialTransaction().withItems(state, Command(Commands.Register(), owner.party.owningKey))
} }
/** /**
@ -157,13 +156,13 @@ class CrowdFund : Contract {
tx.addOutputState(campaign.state.copy( tx.addOutputState(campaign.state.copy(
pledges = campaign.state.pledges + CrowdFund.Pledge(subscriber, 1000.DOLLARS) pledges = campaign.state.pledges + CrowdFund.Pledge(subscriber, 1000.DOLLARS)
)) ))
tx.addArg(WireCommand(Commands.Pledge(), subscriber)) tx.addCommand(Commands.Pledge(), subscriber)
} }
fun craftClose(tx: PartialTransaction, campaign: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) { fun craftClose(tx: PartialTransaction, campaign: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
tx.addInputState(campaign.ref) tx.addInputState(campaign.ref)
tx.addOutputState(campaign.state.copy(closed = true)) tx.addOutputState(campaign.state.copy(closed = true))
tx.addArg(WireCommand(Commands.Close(), campaign.state.campaign.owner)) tx.addCommand(Commands.Close(), campaign.state.campaign.owner)
// If campaign target has not been met, compose cash returns // If campaign target has not been met, compose cash returns
if (campaign.state.pledgedAmount < campaign.state.campaign.target) { if (campaign.state.pledgedAmount < campaign.state.campaign.target) {
for (pledge in campaign.state.pledges) { for (pledge in campaign.state.pledges) {

View File

@ -91,7 +91,7 @@ public class JavaCommercialPaper implements Contract {
} }
} }
public static class Commands implements core.Command { public static class Commands implements core.CommandData {
public static class Move extends Commands { public static class Move extends Commands {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
@ -114,9 +114,12 @@ public class JavaCommercialPaper implements Contract {
List<InOutGroup<State>> groups = tx.groupStates(State.class, State::withoutOwner); List<InOutGroup<State>> groups = tx.groupStates(State.class, State::withoutOwner);
// Find the command that instructs us what to do and check there's exactly one. // Find the command that instructs us what to do and check there's exactly one.
AuthenticatedObject<Command> cmd = requireSingleCommand(tx.getCommands(), Commands.class); AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
Instant time = tx.getTime(); // Can be null/missing. TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity());
if (timestampCommand == null)
throw new IllegalArgumentException("must be timestamped");
Instant time = timestampCommand.getMidpoint();
for (InOutGroup<State> group : groups) { for (InOutGroup<State> group : groups) {
List<State> inputs = group.getInputs(); List<State> inputs = group.getInputs();
@ -138,8 +141,6 @@ public class JavaCommercialPaper implements Contract {
throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner"); throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner");
} else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) { } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) {
Amount received = CashKt.sumCashOrNull(inputs); Amount received = CashKt.sumCashOrNull(inputs);
if (time == null)
throw new IllegalArgumentException("Redemption transactions must be timestamped");
if (received == null) if (received == null)
throw new IllegalStateException("Failed requirement: no cash being redeemed"); throw new IllegalStateException("Failed requirement: no cash being redeemed");
if (input.getMaturityDate().isAfter(time)) if (input.getMaturityDate().isAfter(time))

View File

@ -58,8 +58,8 @@ abstract class TwoPartyTradeProtocol {
abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>() abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<WireTransaction, LedgerTransaction>>()
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>() abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<WireTransaction, LedgerTransaction>>()
companion object { companion object {
@JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol { @JvmStatic fun create(smm: StateMachineManager): TwoPartyTradeProtocol {
@ -89,7 +89,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
// the continuation by the state machine framework. Please refer to the documentation website (docs/build/html) to // the continuation by the state machine framework. Please refer to the documentation website (docs/build/html) to
// learn more about the protocol state machine framework. // learn more about the protocol state machine framework.
class SellerImpl : Seller() { class SellerImpl : Seller() {
override fun call(args: SellerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> { override fun call(args: SellerInitialArgs): Pair<WireTransaction, LedgerTransaction> {
val sessionID = random63BitValue() val sessionID = random63BitValue()
// Make the first message we'll send to kick off the protocol. // Make the first message we'll send to kick off the protocol.
@ -99,7 +99,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
logger().trace { "Received partially signed transaction" } logger().trace { "Received partially signed transaction" }
partialTX.verifySignatures() partialTX.verifySignatures()
val wtx = partialTX.txBits.deserialize<WireTransaction>() val wtx: WireTransaction = partialTX.txBits.deserialize()
requireThat { requireThat {
"transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price) "transaction sends us the right amount of cash" by (wtx.outputStates.sumCashBy(args.myKeyPair.public) == args.price)
@ -116,16 +116,15 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
// express protocol state machines on top of the messaging layer. // express protocol state machines on top of the messaging layer.
} }
val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits.bits) val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits)
val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature) val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature)
// We should run it through our full TransactionGroup of all transactions here. // We should run it through our full TransactionGroup of all transactions here.
fullySigned.verify() fullySigned.verify()
val timestamped: TimestampedWireTransaction = fullySigned.toTimestampedTransaction(serviceHub.timestampingService)
logger().trace { "Built finished transaction, sending back to secondary!" } logger().trace { "Built finished transaction, sending back to secondary!" }
send(TRADE_TOPIC, args.buyerSessionID, timestamped) send(TRADE_TOPIC, args.buyerSessionID, fullySigned)
return Pair(timestamped, timestamped.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService)) return Pair(wtx, fullySigned.verifyToLedgerTransaction(serviceHub.identityService))
} }
} }
@ -136,7 +135,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
// The buyer's side of the protocol. See note above Seller to learn about the caveats here. // The buyer's side of the protocol. See note above Seller to learn about the caveats here.
class BuyerImpl : Buyer() { class BuyerImpl : Buyer() {
override fun call(args: BuyerInitialArgs): Pair<TimestampedWireTransaction, LedgerTransaction> { override fun call(args: BuyerInitialArgs): Pair<WireTransaction, LedgerTransaction> {
// Wait for a trade request to come in on our pre-provided session ID. // Wait for a trade request to come in on our pre-provided session ID.
val tradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, args.sessionID) val tradeRequest = receive<SellerTradeInfo>(TRADE_TOPIC, args.sessionID)
@ -167,7 +166,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
val freshKey = serviceHub.keyManagementService.freshKey() val freshKey = serviceHub.keyManagementService.freshKey()
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public) val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
ptx.addOutputState(state) ptx.addOutputState(state)
ptx.addArg(WireCommand(command, tradeRequest.assetForSale.state.owner)) ptx.addCommand(command, tradeRequest.assetForSale.state.owner)
// Now sign the transaction with whatever keys we need to move the cash. // Now sign the transaction with whatever keys we need to move the cash.
for (k in cashSigningPubKeys) { for (k in cashSigningPubKeys) {
@ -184,16 +183,15 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
// TODO: Protect against the buyer terminating here and leaving us in the lurch without the final tx. // 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. // TODO: Protect against a malicious buyer sending us back a different transaction to the one we built.
val fullySigned = sendAndReceive<TimestampedWireTransaction>(TRADE_TOPIC, val fullySigned = sendAndReceive<SignedWireTransaction>(TRADE_TOPIC, tradeRequest.sessionID, args.sessionID, stx)
tradeRequest.sessionID, args.sessionID, stx)
logger().trace { "Got fully signed transaction, verifying ... "} logger().trace { "Got fully signed transaction, verifying ... "}
val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.timestampingService, serviceHub.identityService) val ltx = fullySigned.verifyToLedgerTransaction(serviceHub.identityService)
logger().trace { "Fully signed transaction was valid. Trade complete! :-)" } logger().trace { "Fully signed transaction was valid. Trade complete! :-)" }
return Pair(fullySigned, ltx) return Pair(fullySigned.verify(), ltx)
} }
} }

View File

@ -106,17 +106,24 @@ fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) s
//// Authenticated commands /////////////////////////////////////////////////////////////////////////////////////////// //// Authenticated commands ///////////////////////////////////////////////////////////////////////////////////////////
/** Filters the command list by type, party and public key all at once. */ /** Filters the command list by type, party and public key all at once. */
inline fun <reified T : Command> List<AuthenticatedObject<Command>>.select(signer: PublicKey? = null, party: Party? = null) = inline fun <reified T : CommandData> List<AuthenticatedObject<CommandData>>.select(signer: PublicKey? = null, party: Party? = null) =
filter { it.value is T }. filter { it.value is T }.
filter { if (signer == null) true else it.signers.contains(signer) }. filter { if (signer == null) true else it.signers.contains(signer) }.
filter { if (party == null) true else it.signingParties.contains(party) }. filter { if (party == null) true else it.signingParties.contains(party) }.
map { AuthenticatedObject<T>(it.signers, it.signingParties, it.value as T) } map { AuthenticatedObject<T>(it.signers, it.signingParties, it.value as T) }
inline fun <reified T : Command> List<AuthenticatedObject<Command>>.requireSingleCommand() = try { inline fun <reified T : CommandData> List<AuthenticatedObject<CommandData>>.requireSingleCommand() = try {
select<T>().single() select<T>().single()
} catch (e: NoSuchElementException) { } catch (e: NoSuchElementException) {
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message. throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
} }
// For Java // For Java
fun List<AuthenticatedObject<Command>>.requireSingleCommand(klass: Class<out Command>) = filter { klass.isInstance(it) }.single() fun List<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<out CommandData>) = filter { klass.isInstance(it) }.single()
/** Returns a timestamp that was signed by the given authority, or returns null if missing. */
fun List<AuthenticatedObject<CommandData>>.getTimestampBy(timestampingAuthority: Party): TimestampCommand? {
val timestampCmds = filter { it.signers.contains(timestampingAuthority.owningKey) && it.value is TimestampCommand }
return timestampCmds.singleOrNull()?.value as? TimestampCommand
}

View File

@ -60,7 +60,6 @@ open class DigitalSignature(bits: ByteArray, val covering: Int = 0) : OpaqueByte
} }
class LegallyIdentifiable(val signer: Party, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering) class LegallyIdentifiable(val signer: Party, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
} }
object NullPublicKey : PublicKey, Comparable<PublicKey> { object NullPublicKey : PublicKey, Comparable<PublicKey> {
@ -90,8 +89,16 @@ fun PrivateKey.signWithECDSA(bits: ByteArray): DigitalSignature {
return DigitalSignature(sig) return DigitalSignature(sig)
} }
fun PrivateKey.signWithECDSA(bits: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, signWithECDSA(bits).bits) fun PrivateKey.signWithECDSA(bitsToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey {
fun KeyPair.signWithECDSA(bits: ByteArray) = private.signWithECDSA(bits, public) return DigitalSignature.WithKey(publicKey, signWithECDSA(bitsToSign).bits)
}
fun KeyPair.signWithECDSA(bitsToSign: ByteArray) = private.signWithECDSA(bitsToSign, public)
fun KeyPair.signWithECDSA(bitsToSign: OpaqueBytes) = private.signWithECDSA(bitsToSign.bits, public)
fun KeyPair.signWithECDSA(bitsToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable {
check(public == party.owningKey)
val sig = signWithECDSA(bitsToSign)
return DigitalSignature.LegallyIdentifiable(party, sig.bits, 0)
}
/** Utility to simplify the act of verifying a signature */ /** Utility to simplify the act of verifying a signature */
fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) { fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) {

View File

@ -9,10 +9,11 @@
package core package core
import core.messaging.MessagingService import core.messaging.MessagingService
import core.serialization.SerializedBytes
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
import java.time.Instant
/** /**
* This file defines various 'services' which are not currently fleshed out. A service is a module that provides * This file defines various 'services' which are not currently fleshed out. A service is a module that provides
@ -70,15 +71,25 @@ interface IdentityService {
} }
/** /**
* Simple interface (for testing) to an abstract timestamping service, in the style of RFC 3161. Note that this is not * Simple interface (for testing) to an abstract timestamping service. Note that this is not "timestamping" in the
* 'timestamping' in the block chain sense, but rather, implies a semi-trusted third party taking a reading of the * blockchain sense of a total ordering of transactions, but rather, a signature from a well known/trusted timestamping
* current time, typically from an atomic clock, and then digitally signing (current time, hash) to produce a timestamp * service over a transaction that indicates the timestamp in it is accurate. Such a signature may not always be
* triple (signature, time, hash). The purpose of these timestamps is to locate a transaction in the timeline, which is * necessary: if there are multiple parties involved in a transaction then they can cross-check the timestamp
* important in the absence of blocks. Here we model the timestamp as an opaque byte array. * themselves.
*/ */
interface TimestamperService { interface TimestamperService {
fun timestamp(hash: SecureHash): ByteArray fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable
fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant
/** The name+pubkey that this timestamper will sign with. */
val identity: Party
}
// Smart contracts may wish to specify explicitly which timestamping authorities are trusted to assert the time.
// 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 identity = Party("The dummy timestamper", key.public)
} }
/** /**

View File

@ -11,6 +11,8 @@ package core
import core.serialization.OpaqueBytes import core.serialization.OpaqueBytes
import core.serialization.serialize import core.serialization.serialize
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration
import java.time.Instant
/** /**
* A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk * A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk
@ -31,7 +33,7 @@ interface OwnableState : ContractState {
val owner: PublicKey val owner: PublicKey
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */ /** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
fun withNewOwner(newOwner: PublicKey): Pair<Command, OwnableState> fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState>
} }
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */ /** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
@ -63,14 +65,19 @@ data class PartyReference(val party: Party, val reference: OpaqueBytes) {
} }
/** Marker interface for classes that represent commands */ /** Marker interface for classes that represent commands */
interface Command interface CommandData
/** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */ /** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */
abstract class TypeOnlyCommand : Command { abstract class TypeOnlyCommandData : CommandData {
override fun equals(other: Any?) = other?.javaClass == javaClass override fun equals(other: Any?) = other?.javaClass == javaClass
override fun hashCode() = javaClass.name.hashCode() override fun hashCode() = javaClass.name.hashCode()
} }
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
data class Command(val data: CommandData, val pubkeys: List<PublicKey>) {
constructor(data: CommandData, key: PublicKey) : this(data, listOf(key))
}
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */ /** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
data class AuthenticatedObject<out T : Any>( data class AuthenticatedObject<out T : Any>(
val signers: List<PublicKey>, val signers: List<PublicKey>,
@ -79,6 +86,23 @@ data class AuthenticatedObject<out T : Any>(
val value: T val value: T
) )
/**
* If present in a transaction, contains a time that was verified by the timestamping authority/authorities whose
* public keys are identified in the containing [Command] object.
*/
data class TimestampCommand(val after: Instant?, val before: Instant?) : CommandData {
init {
if (after == null && before == null)
throw IllegalArgumentException("At least one of before/after must be specified")
if (after != null && before != null)
check(after <= before)
}
constructor(time: Instant, tolerance: Duration) : this(time - tolerance, time + tolerance)
val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2)
}
/** /**
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for * Implemented by a program that implements business logic on the shared ledger. All participants run this code for
* every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the * every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the

View File

@ -49,7 +49,7 @@ class TransactionGroup(val transactions: Set<LedgerTransaction>, val nonVerified
// Look up the output in that transaction by index. // Look up the output in that transaction by index.
inputs.add(ltx.outStates[ref.index]) inputs.add(ltx.outStates[ref.index])
} }
resolved.add(TransactionForVerification(inputs, tx.outStates, tx.commands, tx.time, tx.hash)) resolved.add(TransactionForVerification(inputs, tx.outStates, tx.commands, tx.hash))
} }
for (tx in resolved) for (tx in resolved)

View File

@ -8,10 +8,14 @@
package core package core
import core.serialization.* import core.serialization.SerializedBytes
import core.serialization.deserialize
import core.serialization.serialize
import java.security.KeyPair import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException import java.security.SignatureException
import java.time.Clock
import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -19,26 +23,23 @@ import java.util.*
* Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object * Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object
* tree passed into a contract. * tree passed into a contract.
* *
* TimestampedWireTransaction wraps a serialized SignedWireTransaction. The timestamp is a signature from a timestamping
* authority and is what gives the contract a sense of time. This arrangement may change in future.
*
* SignedWireTransaction wraps a serialized WireTransaction. It contains one or more ECDSA signatures, each one from * SignedWireTransaction wraps a serialized WireTransaction. It contains one or more ECDSA signatures, each one from
* a public key that is mentioned inside a transaction command. * a public key that is mentioned inside a transaction command.
* *
* WireTransaction is a transaction in a form ready to be serialised/unserialised. A WireTransaction can be hashed * WireTransaction is a transaction in a form ready to be serialised/unserialised. A WireTransaction can be hashed
* in various ways to calculate a *signature hash* (or sighash), this is the hash that is signed by the various involved * in various ways to calculate a *signature hash* (or sighash), this is the hash that is signed by the various involved
* keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of a * keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of a SignedWireTransaction
* TimestampedWireTransaction i.e. the outermost serialised form with everything included. * i.e. the outermost serialised form with everything included.
* *
* A PartialTransaction is a transaction class that's mutable (unlike the others which are all immutable). It is * A PartialTransaction is a transaction class that's mutable (unlike the others which are all immutable). It is
* intended to be passed around contracts that may edit it by adding new states/commands or modifying the existing set. * intended to be passed around contracts that may edit it by adding new states/commands or modifying the existing set.
* Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from * Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from
* multiple parties. * multiple parties.
* *
* LedgerTransaction is derived from WireTransaction and TimestampedWireTransaction together. It is the result of * LedgerTransaction is derived from WireTransaction. It is the result of doing some basic key lookups on WireCommand
* doing some basic key lookups on WireCommand to see if any keys are from a recognised party, thus converting * to see if any keys are from a recognised party, thus converting the WireCommand objects into
* the WireCommand objects into AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party * AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party map. In future it'd make more
* map. In future it'd make more sense to use a certificate scheme and so that logic would get more complex. * sense to use a certificate scheme and so that logic would get more complex.
* *
* All the above refer to inputs using a (txhash, output index) pair. * All the above refer to inputs using a (txhash, output index) pair.
* *
@ -46,35 +47,57 @@ import java.util.*
* database and replaced with the real objects. TFV is the form that is finally fed into the contracts. * database and replaced with the real objects. TFV is the form that is finally fed into the contracts.
*/ */
/** Serialized command plus pubkey pair: the signature is stored at the end of the serialized bytes */
data class WireCommand(val command: Command, val pubkeys: List<PublicKey>) {
constructor(command: Command, key: PublicKey) : this(command, listOf(key))
}
/** Transaction ready for serialisation, without any signatures attached. */ /** Transaction ready for serialisation, without any signatures attached. */
data class WireTransaction(val inputStates: List<ContractStateRef>, data class WireTransaction(val inputStates: List<ContractStateRef>,
val outputStates: List<ContractState>, val outputStates: List<ContractState>,
val commands: List<WireCommand>) { val commands: List<Command>) {
fun toLedgerTransaction(timestamp: Instant?, identityService: IdentityService, originalHash: SecureHash): LedgerTransaction { fun toLedgerTransaction(identityService: IdentityService, originalHash: SecureHash): LedgerTransaction {
val authenticatedArgs = commands.map { val authenticatedArgs = commands.map {
val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) } val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) }
AuthenticatedObject(it.pubkeys, institutions, it.command) AuthenticatedObject(it.pubkeys, institutions, it.data)
} }
return LedgerTransaction(inputStates, outputStates, authenticatedArgs, timestamp, originalHash) return LedgerTransaction(inputStates, outputStates, authenticatedArgs, originalHash)
} }
} }
/**
* Thrown if an attempt is made to timestamp a transaction using a trusted timestamper, but the time on the transaction
* is too far in the past or future relative to the local clock and thus the timestamper would reject it.
*/
class NotOnTimeException : Exception()
/** A mutable transaction that's in the process of being built, before all signatures are present. */ /** A mutable transaction that's in the process of being built, before all signatures are present. */
class PartialTransaction(private val inputStates: MutableList<ContractStateRef> = arrayListOf(), class PartialTransaction(private val inputStates: MutableList<ContractStateRef> = arrayListOf(),
private val outputStates: MutableList<ContractState> = arrayListOf(), private val outputStates: MutableList<ContractState> = arrayListOf(),
private val commands: MutableList<WireCommand> = arrayListOf()) { private val commands: MutableList<Command> = arrayListOf()) {
val time: TimestampCommand? get() = commands.mapNotNull { it.data as? TimestampCommand }.singleOrNull()
/**
* Places a [TimestampCommand] in this transaction, removing any existing command if there is one.
* To get the right signature from the timestamping service, use the [timestamp] method after building is
* finished.
*
* The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance].
* If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance
* should be chosen such that your code can finish building the transaction and sending it to the TSA within that
* window of time, taking into account factors such as network latency. Transactions being built by a group of
* collaborating parties may therefore require a higher time tolerance than a transaction being built by a single
* node.
*/
fun setTime(time: Instant, authenticatedBy: Party, timeTolerance: Duration) {
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
commands.removeAll { it.data is TimestampCommand }
addCommand(TimestampCommand(time, timeTolerance), authenticatedBy.owningKey)
}
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */ /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
public fun withItems(vararg items: Any): PartialTransaction { public fun withItems(vararg items: Any): PartialTransaction {
for (t in items) { for (t in items) {
when (t) { when (t) {
is ContractStateRef -> inputStates.add(t) is ContractStateRef -> inputStates.add(t)
is ContractState -> outputStates.add(t) is ContractState -> outputStates.add(t)
is WireCommand -> commands.add(t) is Command -> commands.add(t)
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
} }
} }
@ -88,16 +111,45 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" } check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
check(commands.count { it.pubkeys.contains(key.public) } > 0) { "Trying to sign with a key that isn't in any command" } check(commands.count { it.pubkeys.contains(key.public) } > 0) { "Trying to sign with a key that isn't in any command" }
val data = toWireTransaction().serialize() val data = toWireTransaction().serialize()
currentSigs.add(key.private.signWithECDSA(data.bits, key.public)) currentSigs.add(key.signWithECDSA(data.bits))
} }
fun toWireTransaction() = WireTransaction(inputStates, outputStates, commands) /**
* Uses the given timestamper service to request a signature over the WireTransaction be added. There must always be
* at least one such signature, but others may be added as well. You may want to have multiple redundant timestamps
* in the following cases:
*
* - Cross border contracts where local law says that only local timestamping authorities are acceptable.
* - Backup in case a TSA's signing key is compromised.
*
* The signature of the trusted timestamper merely asserts that the time field of this transaction is valid.
*/
fun timestamp(timestamper: TimestamperService, clock: Clock = Clock.systemUTC()) {
// TODO: Once we switch to a more advanced bytecode rewriting framework, we can call into a real implementation.
check(timestamper.javaClass.simpleName == "DummyTimestamper")
val t = time ?: throw IllegalStateException("Timestamping requested but no time was inserted into the transaction")
// Obviously this is just a hard-coded dummy value for now.
val maxExpectedLatency = 5.seconds
if (Duration.between(clock.instant(), t.before) > maxExpectedLatency)
throw NotOnTimeException()
// The timestamper may also throw NotOnTimeException if our clocks are desynchronised or if we are right on the
// boundary of t.notAfter and network latency pushes us over the edge. By "synchronised" here we mean relative
// to GPS time i.e. the United States Naval Observatory.
val sig = timestamper.timestamp(toWireTransaction().serialize())
currentSigs.add(sig)
}
fun toWireTransaction() = WireTransaction(ArrayList(inputStates), ArrayList(outputStates), ArrayList(commands))
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedWireTransaction { fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedWireTransaction {
if (checkSufficientSignatures) { if (checkSufficientSignatures) {
val requiredKeys = commands.flatMap { it.pubkeys }.toSet()
val gotKeys = currentSigs.map { it.by }.toSet() val gotKeys = currentSigs.map { it.by }.toSet()
check(gotKeys == requiredKeys) { "The set of required signatures isn't equal to the signatures we've got" } for (command in commands) {
if (!gotKeys.containsAll(command.pubkeys))
throw IllegalStateException("Missing signatures on the transaction for a ${command.data.javaClass.canonicalName} command")
}
} }
return SignedWireTransaction(toWireTransaction().serialize(), ArrayList(currentSigs)) return SignedWireTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
} }
@ -112,17 +164,20 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
outputStates.add(state) outputStates.add(state)
} }
fun addArg(arg: WireCommand) { fun addCommand(arg: Command) {
check(currentSigs.isEmpty()) check(currentSigs.isEmpty())
// We should probably merge the lists of pubkeys for identical commands here. // We should probably merge the lists of pubkeys for identical commands here.
commands.add(arg) commands.add(arg)
} }
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
// Accessors that yield immutable snapshots. // Accessors that yield immutable snapshots.
fun inputStates(): List<ContractStateRef> = ArrayList(inputStates) fun inputStates(): List<ContractStateRef> = ArrayList(inputStates)
fun outputStates(): List<ContractState> = ArrayList(outputStates) fun outputStates(): List<ContractState> = ArrayList(outputStates)
fun commands(): List<WireCommand> = ArrayList(commands) fun commands(): List<Command> = ArrayList(commands)
} }
data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, val sigs: List<DigitalSignature.WithKey>) { data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, val sigs: List<DigitalSignature.WithKey>) {
@ -155,43 +210,17 @@ data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, v
// unverified. // unverified.
val cmdKeys = wtx.commands.flatMap { it.pubkeys }.toSet() val cmdKeys = wtx.commands.flatMap { it.pubkeys }.toSet()
val sigKeys = sigs.map { it.by }.toSet() val sigKeys = sigs.map { it.by }.toSet()
if (cmdKeys != sigKeys) if (!sigKeys.containsAll(cmdKeys))
throw SignatureException("Command keys don't match the signatures: $cmdKeys vs $sigKeys") throw SignatureException("Missing signatures on the transaction for: ${cmdKeys - sigKeys}")
return wtx return wtx
} }
/** Uses the given timestamper service to calculate a signed timestamp and then returns a wrapper for both */ /**
fun toTimestampedTransaction(timestamper: TimestamperService): TimestampedWireTransaction { * Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call
val bits = serialize() * [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys.
return TimestampedWireTransaction(bits, timestamper.timestamp(bits.sha256()).opaque())
}
/** Returns a [TimestampedWireTransaction] with an empty byte array as the timestamp: this means, no time was provided. */
fun toTimestampedTransactionWithoutTime() = TimestampedWireTransaction(serialize(), null)
}
/**
* A TimestampedWireTransaction is the outermost, final form that a transaction takes. The hash of this structure is
* how transactions are identified on the network and in the ledger.
*/ */
data class TimestampedWireTransaction( fun verifyToLedgerTransaction(identityService: IdentityService): LedgerTransaction {
/** A serialised SignedWireTransaction */ return verify().toLedgerTransaction(identityService, txBits.bits.sha256())
val signedWireTXBytes: SerializedBytes<SignedWireTransaction>,
/** Signature from a timestamping authority. For instance using RFC 3161 */
val timestamp: OpaqueBytes?
) {
val transactionID: SecureHash = serialize().sha256()
fun verifyToLedgerTransaction(timestamper: TimestamperService, identityService: IdentityService): LedgerTransaction {
val stx = signedWireTXBytes.deserialize()
val wtx: WireTransaction = stx.verify()
val instant: Instant? =
if (timestamp != null)
timestamper.verifyTimestamp(signedWireTXBytes.sha256(), timestamp.bits)
else
null
return wtx.toLedgerTransaction(instant, identityService, transactionID)
} }
} }
@ -206,12 +235,9 @@ data class LedgerTransaction(
/** The states that will be generated by the execution of this transaction. */ /** The states that will be generated by the execution of this transaction. */
val outStates: List<ContractState>, val outStates: List<ContractState>,
/** Arbitrary data passed to the program of each input state. */ /** Arbitrary data passed to the program of each input state. */
val commands: List<AuthenticatedObject<Command>>, val commands: List<AuthenticatedObject<CommandData>>,
/** The moment the transaction was timestamped for, if a timestamp was present. */
val time: Instant?,
/** The hash of the original serialised TimestampedWireTransaction or SignedTransaction */ /** The hash of the original serialised TimestampedWireTransaction or SignedTransaction */
val hash: SecureHash val hash: SecureHash
// TODO: nLockTime equivalent?
) { ) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int) = StateAndRef(outStates[index] as T, ContractStateRef(hash, index)) fun <T : ContractState> outRef(index: Int) = StateAndRef(outStates[index] as T, ContractStateRef(hash, index))
@ -224,11 +250,11 @@ data class LedgerTransaction(
} }
} }
// TODO: Move class this into TransactionGroup.kt
/** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */ /** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */
data class TransactionForVerification(val inStates: List<ContractState>, data class TransactionForVerification(val inStates: List<ContractState>,
val outStates: List<ContractState>, val outStates: List<ContractState>,
val commands: List<AuthenticatedObject<Command>>, val commands: List<AuthenticatedObject<CommandData>>,
val time: Instant?,
val origHash: SecureHash) { val origHash: SecureHash) {
override fun hashCode() = origHash.hashCode() override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
@ -258,6 +284,9 @@ data class TransactionForVerification(val inStates: List<ContractState>,
data class InOutGroup<T : ContractState>(val inputs: List<T>, val outputs: List<T>) data class InOutGroup<T : ContractState>(val inputs: List<T>, val outputs: List<T>)
// A shortcut to make IDE auto-completion more intuitive for Java users.
fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority)
// For Java users. // For Java users.
fun <T : ContractState> groupStates(ofType: Class<T>, selector: (T) -> Any): List<InOutGroup<T>> { fun <T : ContractState> groupStates(ofType: Class<T>, selector: (T) -> Any): List<InOutGroup<T>> {
val inputs = inStates.filterIsInstance(ofType) val inputs = inStates.filterIsInstance(ofType)

View File

@ -110,7 +110,7 @@ class CashTests {
assertEquals(100.DOLLARS, s.amount) assertEquals(100.DOLLARS, s.amount)
assertEquals(MINI_CORP, s.deposit.party) assertEquals(MINI_CORP, s.deposit.party)
assertEquals(DUMMY_PUBKEY_1, s.owner) assertEquals(DUMMY_PUBKEY_1, s.owner)
assertTrue(ptx.commands()[0].command is Cash.Commands.Issue) assertTrue(ptx.commands()[0].data is Cash.Commands.Issue)
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].pubkeys[0]) assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].pubkeys[0])
} }

View File

@ -11,7 +11,9 @@ package contracts
import core.* import core.*
import core.testutils.* import core.testutils.*
import org.junit.Test import org.junit.Test
import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.ZoneOffset
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -39,6 +41,7 @@ class CommercialPaperTests {
transaction { transaction {
output { PAPER_1 } output { PAPER_1 }
arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() } arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
timestamp(TEST_TX_TIME)
} }
expectFailureOfTx(1, "signed by the claimed issuer") expectFailureOfTx(1, "signed by the claimed issuer")
@ -51,6 +54,7 @@ class CommercialPaperTests {
transaction { transaction {
output { PAPER_1.copy(faceValue = 0.DOLLARS) } output { PAPER_1.copy(faceValue = 0.DOLLARS) }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
timestamp(TEST_TX_TIME)
} }
expectFailureOfTx(1, "face value is not zero") expectFailureOfTx(1, "face value is not zero")
@ -63,12 +67,35 @@ class CommercialPaperTests {
transaction { transaction {
output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) } output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
timestamp(TEST_TX_TIME)
} }
expectFailureOfTx(1, "maturity date is not in the past") expectFailureOfTx(1, "maturity date is not in the past")
} }
} }
@Test
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 {
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
signWith(MINI_CORP_KEY)
assertFailsWith(NotOnTimeException::class) {
timestamp(DummyTimestamper(Clock.fixed(TEST_TX_TIME + 5.hours, ZoneOffset.UTC)))
}
}
// 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 {
setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
signWith(MINI_CORP_KEY)
assertFailsWith(NotOnTimeException::class) {
val tsaClock = Clock.fixed(TEST_TX_TIME - 5.hours, ZoneOffset.UTC)
timestamp(DummyTimestamper(tsaClock), Clock.fixed(TEST_TX_TIME, ZoneOffset.UTC))
}
}
}
@Test @Test
fun `issue cannot replace an existing state`() { fun `issue cannot replace an existing state`() {
transactionGroup { transactionGroup {
@ -79,6 +106,7 @@ class CommercialPaperTests {
input("paper") input("paper")
output { PAPER_1 } output { PAPER_1 }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
timestamp(TEST_TX_TIME)
} }
expectFailureOfTx(1, "there is no input state") expectFailureOfTx(1, "there is no input state")
@ -96,7 +124,7 @@ class CommercialPaperTests {
} }
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> { fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), TEST_TX_TIME, SecureHash.randomSHA256()) 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, ContractStateRef(ltx.hash, index)) })
} }
@ -104,10 +132,13 @@ class CommercialPaperTests {
fun `issue move and then redeem`() { fun `issue move and then redeem`() {
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. // MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
val issueTX: LedgerTransaction = run { val issueTX: LedgerTransaction = run {
val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days) val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days).apply {
ptx.signWith(MINI_CORP_KEY) setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
signWith(MINI_CORP_KEY)
timestamp(DUMMY_TIMESTAMPER)
}
val stx = ptx.toSignedTransaction() val stx = ptx.toSignedTransaction()
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256()) stx.verifyToLedgerTransaction(MockIdentityService)
} }
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet( val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
@ -124,7 +155,7 @@ class CommercialPaperTests {
ptx.signWith(MINI_CORP_KEY) ptx.signWith(MINI_CORP_KEY)
ptx.signWith(ALICE_KEY) ptx.signWith(ALICE_KEY)
val stx = ptx.toSignedTransaction() val stx = ptx.toSignedTransaction()
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256()) stx.verifyToLedgerTransaction(MockIdentityService)
} }
// Won't be validated. // Won't be validated.
@ -135,10 +166,12 @@ class CommercialPaperTests {
fun makeRedeemTX(time: Instant): LedgerTransaction { fun makeRedeemTX(time: Instant): LedgerTransaction {
val ptx = PartialTransaction() val ptx = PartialTransaction()
ptx.setTime(time, DummyTimestampingAuthority.identity, 30.seconds)
CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet) CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet)
ptx.signWith(ALICE_KEY) ptx.signWith(ALICE_KEY)
ptx.signWith(MINI_CORP_KEY) ptx.signWith(MINI_CORP_KEY)
return ptx.toSignedTransaction().verify().toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256()) ptx.timestamp(DUMMY_TIMESTAMPER)
return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService)
} }
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days) val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
@ -167,6 +200,7 @@ class CommercialPaperTests {
transaction("Issuance") { transaction("Issuance") {
output("paper") { PAPER_1 } output("paper") { PAPER_1 }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
timestamp(TEST_TX_TIME)
} }
// The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days, // The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
@ -182,7 +216,7 @@ class CommercialPaperTests {
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200 // Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
// as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change. // as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
transaction("Redemption", redemptionTime) { transaction("Redemption") {
input("alice's paper") input("alice's paper")
input("some profits") input("some profits")
@ -193,6 +227,8 @@ class CommercialPaperTests {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
arg(ALICE) { CommercialPaper.Commands.Redeem() } arg(ALICE) { CommercialPaper.Commands.Redeem() }
timestamp(redemptionTime)
} }
} }
} }

View File

@ -34,6 +34,7 @@ class CrowdFundTests {
transaction { transaction {
output { CF_1 } output { CF_1 }
arg(DUMMY_PUBKEY_1) { CrowdFund.Commands.Register() } arg(DUMMY_PUBKEY_1) { CrowdFund.Commands.Register() }
timestamp(TEST_TX_TIME)
} }
expectFailureOfTx(1, "the transaction is signed by the owner of the crowdsourcing") expectFailureOfTx(1, "the transaction is signed by the owner of the crowdsourcing")
@ -46,6 +47,7 @@ class CrowdFundTests {
transaction { transaction {
output { CF_1.copy(campaign = CF_1.campaign.copy(closingTime = TEST_TX_TIME - 1.days)) } output { CF_1.copy(campaign = CF_1.campaign.copy(closingTime = TEST_TX_TIME - 1.days)) }
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() } arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
timestamp(TEST_TX_TIME)
} }
expectFailureOfTx(1, "the output registration has a closing time in the future") expectFailureOfTx(1, "the output registration has a closing time in the future")
@ -67,6 +69,7 @@ class CrowdFundTests {
transaction { transaction {
output("funding opportunity") { CF_1 } output("funding opportunity") { CF_1 }
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() } arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
timestamp(TEST_TX_TIME)
} }
// 2. Place a pledge // 2. Place a pledge
@ -81,19 +84,21 @@ class CrowdFundTests {
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY } output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
arg(ALICE) { Cash.Commands.Move() } arg(ALICE) { Cash.Commands.Move() }
arg(ALICE) { CrowdFund.Commands.Pledge() } arg(ALICE) { CrowdFund.Commands.Pledge() }
timestamp(TEST_TX_TIME)
} }
// 3. Close the opportunity, assuming the target has been met // 3. Close the opportunity, assuming the target has been met
transaction(time = TEST_TX_TIME + 8.days) { transaction {
input ("pledged opportunity") input ("pledged opportunity")
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) } output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Close() } arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Close() }
timestamp(time = TEST_TX_TIME + 8.days)
} }
} }
} }
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> { fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), TEST_TX_TIME, SecureHash.randomSHA256()) 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, ContractStateRef(ltx.hash, index)) })
} }
@ -102,10 +107,13 @@ class CrowdFundTests {
// MiniCorp registers a crowdfunding of $1,000, to close in 7 days. // MiniCorp registers a crowdfunding of $1,000, to close in 7 days.
val registerTX: LedgerTransaction = run { val registerTX: LedgerTransaction = run {
// craftRegister returns a partial transaction // craftRegister returns a partial transaction
val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days) val ptx = CrowdFund().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days).apply {
ptx.signWith(MINI_CORP_KEY) setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
signWith(MINI_CORP_KEY)
timestamp(DUMMY_TIMESTAMPER)
}
val stx = ptx.toSignedTransaction() val stx = ptx.toSignedTransaction()
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256()) stx.verifyToLedgerTransaction(MockIdentityService)
} }
// let's give Alice some funds that she can invest // let's give Alice some funds that she can invest
@ -120,10 +128,12 @@ class CrowdFundTests {
val ptx = PartialTransaction() val ptx = PartialTransaction()
CrowdFund().craftPledge(ptx, registerTX.outRef(0), ALICE) CrowdFund().craftPledge(ptx, registerTX.outRef(0), ALICE)
Cash().craftSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet) Cash().craftSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet)
ptx.setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds)
ptx.signWith(ALICE_KEY) ptx.signWith(ALICE_KEY)
ptx.timestamp(DUMMY_TIMESTAMPER)
val stx = ptx.toSignedTransaction() val stx = ptx.toSignedTransaction()
// this verify passes - the transaction contains an output cash, necessary to verify the fund command // this verify passes - the transaction contains an output cash, necessary to verify the fund command
stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256()) stx.verifyToLedgerTransaction(MockIdentityService)
} }
// Won't be validated. // Won't be validated.
@ -134,10 +144,12 @@ class CrowdFundTests {
// MiniCorp closes their campaign. // MiniCorp closes their campaign.
fun makeFundedTX(time: Instant): LedgerTransaction { fun makeFundedTX(time: Instant): LedgerTransaction {
val ptx = PartialTransaction() val ptx = PartialTransaction()
ptx.setTime(time, DUMMY_TIMESTAMPER.identity, 30.seconds)
CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet) CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
ptx.signWith(MINI_CORP_KEY) ptx.signWith(MINI_CORP_KEY)
ptx.timestamp(DUMMY_TIMESTAMPER)
val stx = ptx.toSignedTransaction() val stx = ptx.toSignedTransaction()
return stx.verify().toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256()) return stx.verifyToLedgerTransaction(MockIdentityService)
} }
val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days) val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days)
@ -150,7 +162,5 @@ class CrowdFundTests {
// This verification passes // This verification passes
TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(TEST_PROGRAM_MAP) TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(TEST_PROGRAM_MAP)
} }
} }

View File

@ -97,7 +97,7 @@ class TransactionGroupTests {
// points nowhere. // points nowhere.
val ref = ContractStateRef(SecureHash.randomSHA256(), 0) val ref = ContractStateRef(SecureHash.randomSHA256(), 0)
tg.txns.add(LedgerTransaction( tg.txns.add(LedgerTransaction(
listOf(ref), listOf(A_THOUSAND_POUNDS), listOf(AuthenticatedObject(listOf(BOB), emptyList(), Cash.Commands.Move())), TEST_TX_TIME, SecureHash.randomSHA256()) listOf(ref), listOf(A_THOUSAND_POUNDS), listOf(AuthenticatedObject(listOf(BOB), emptyList(), Cash.Commands.Move())), SecureHash.randomSHA256())
) )
val e = assertFailsWith(TransactionResolutionException::class) { val e = assertFailsWith(TransactionResolutionException::class) {

View File

@ -16,7 +16,6 @@ import org.junit.Test
import java.security.SignatureException import java.security.SignatureException
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertNull
class TransactionSerializationTests { class TransactionSerializationTests {
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change). // Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
@ -31,7 +30,7 @@ class TransactionSerializationTests {
@Before @Before
fun setup() { fun setup() {
tx = PartialTransaction().withItems( tx = PartialTransaction().withItems(
fakeStateRef, outputState, changeState, WireCommand(Cash.Commands.Move(), arrayListOf(TestUtils.keypair.public)) fakeStateRef, outputState, changeState, Command(Cash.Commands.Move(), arrayListOf(TestUtils.keypair.public))
) )
} }
@ -77,7 +76,7 @@ class TransactionSerializationTests {
// If the signature was replaced in transit, we don't like it. // If the signature was replaced in transit, we don't like it.
assertFailsWith(SignatureException::class) { assertFailsWith(SignatureException::class) {
val tx2 = PartialTransaction().withItems(fakeStateRef, outputState, changeState, val tx2 = PartialTransaction().withItems(fakeStateRef, outputState, changeState,
WireCommand(Cash.Commands.Move(), arrayListOf(TestUtils.keypair2.public))) Command(Cash.Commands.Move(), TestUtils.keypair2.public))
tx2.signWith(TestUtils.keypair2) tx2.signWith(TestUtils.keypair2)
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify() signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
@ -86,18 +85,14 @@ class TransactionSerializationTests {
@Test @Test
fun timestamp() { fun timestamp() {
tx.setTime(TEST_TX_TIME, DUMMY_TIMESTAMPER.identity, 30.seconds)
tx.timestamp(DUMMY_TIMESTAMPER)
tx.signWith(TestUtils.keypair) tx.signWith(TestUtils.keypair)
val ttx = tx.toSignedTransaction().toTimestampedTransactionWithoutTime() val stx = tx.toSignedTransaction()
val ltx = ttx.verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService) val ltx = stx.verifyToLedgerTransaction(MockIdentityService)
assertEquals(tx.commands().map { it.command }, ltx.commands.map { it.value }) assertEquals(tx.commands().map { it.data }, ltx.commands.map { it.value })
assertEquals(tx.inputStates(), ltx.inStateRefs) assertEquals(tx.inputStates(), ltx.inStateRefs)
assertEquals(tx.outputStates(), ltx.outStates) assertEquals(tx.outputStates(), ltx.outStates)
assertNull(ltx.time) assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_TIMESTAMPER.identity)!!.midpoint)
val ltx2: LedgerTransaction = tx.
toSignedTransaction().
toTimestampedTransaction(DUMMY_TIMESTAMPER).
verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService)
assertEquals(TEST_TX_TIME, ltx2.time)
} }
} }

View File

@ -10,20 +10,20 @@
package core.testutils package core.testutils
import com.google.common.io.BaseEncoding
import contracts.* import contracts.*
import core.* import core.*
import core.messaging.MessagingService import core.messaging.MessagingService
import core.serialization.SerializedBytes
import core.serialization.deserialize
import core.visualiser.GraphVisualiser import core.visualiser.GraphVisualiser
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
import java.time.Clock
import java.time.Duration
import java.time.Instant import java.time.Instant
import java.time.ZoneId
import java.util.* import java.util.*
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -67,27 +67,19 @@ val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
) )
/** /**
* A test/mock timestamping service that doesn't use any signatures or security. It always timestamps with * A test/mock timestamping service that doesn't use any signatures or security. It timestamps with
* [TEST_TX_TIME], an arbitrary point on the timeline. * the provided clock which defaults to [TEST_TX_TIME], an arbitrary point on the timeline.
*/ */
class DummyTimestamper(private val time: Instant = TEST_TX_TIME) : TimestamperService { class DummyTimestamper(var clock: Clock = Clock.fixed(TEST_TX_TIME, ZoneId.systemDefault()),
override fun timestamp(hash: SecureHash): ByteArray { val tolerance: Duration = 30.seconds) : TimestamperService {
val bos = ByteArrayOutputStream() override val identity = DummyTimestampingAuthority.identity
DataOutputStream(bos).use {
it.writeLong(time.toEpochMilli())
it.write(hash.bits)
}
return bos.toByteArray()
}
override fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant { override fun timestamp(wtxBytes: SerializedBytes<WireTransaction>): DigitalSignature.LegallyIdentifiable {
val dis = DataInputStream(ByteArrayInputStream(signedTimestamp)) val wtx = wtxBytes.deserialize()
val epochMillis = dis.readLong() val timestamp = wtx.commands.mapNotNull { it.data as? TimestampCommand }.single()
val serHash = ByteArray(32) if (Duration.between(timestamp.before, clock.instant()) > tolerance)
dis.readFully(serHash) throw NotOnTimeException()
if (!Arrays.equals(serHash, hash.bits)) return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity)
throw IllegalStateException("Hash mismatch: ${BaseEncoding.base16().encode(serHash)} vs ${BaseEncoding.base16().encode(hash.bits)}")
return Instant.ofEpochMilli(epochMillis)
} }
} }
@ -176,13 +168,26 @@ infix fun ContractState.label(label: String) = LabeledOutput(label, this)
abstract class AbstractTransactionForTest { abstract class AbstractTransactionForTest {
protected val outStates = ArrayList<LabeledOutput>() protected val outStates = ArrayList<LabeledOutput>()
protected val commands = ArrayList<AuthenticatedObject<Command>>() protected val commands = ArrayList<Command>()
open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) } open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) }
fun arg(vararg key: PublicKey, c: () -> Command) { protected fun commandsToAuthenticatedObjects(): List<AuthenticatedObject<CommandData>> {
return commands.map { AuthenticatedObject(it.pubkeys, it.pubkeys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, it.data) }
}
fun arg(vararg key: PublicKey, c: () -> CommandData) {
val keys = listOf(*key) val keys = listOf(*key)
commands.add(AuthenticatedObject(keys, keys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, c())) commands.add(Command(c(), keys))
}
fun timestamp(time: Instant) {
val data = TimestampCommand(time, 30.seconds)
timestamp(data)
}
fun timestamp(data: TimestampCommand) {
commands.add(Command(data, DUMMY_TIMESTAMPER.identity.owningKey))
} }
// Forbid patterns like: transaction { ... transaction { ... } } // Forbid patterns like: transaction { ... transaction { ... } }
@ -196,7 +201,8 @@ open class TransactionForTest : AbstractTransactionForTest() {
fun input(s: () -> ContractState) = inStates.add(s()) fun input(s: () -> ContractState) = inStates.add(s())
protected fun run(time: Instant) { protected fun run(time: Instant) {
val tx = TransactionForVerification(inStates, outStates.map { it.state }, commands, time, SecureHash.randomSHA256()) val cmds = commandsToAuthenticatedObjects()
val tx = TransactionForVerification(inStates, outStates.map { it.state }, cmds, SecureHash.randomSHA256())
tx.verify(TEST_PROGRAM_MAP) tx.verify(TEST_PROGRAM_MAP)
} }
@ -276,12 +282,12 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
/** /**
* Converts to a [LedgerTransaction] with the givn time, the test institution map, and just assigns a random * Converts to a [LedgerTransaction] with the test institution map, and just assigns a random hash
* hash (i.e. pretend it was signed) * (i.e. pretend it was signed)
*/ */
fun toLedgerTransaction(time: Instant): LedgerTransaction { fun toLedgerTransaction(): LedgerTransaction {
val wireCmds = commands.map { WireCommand(it.value, it.signers) } val wtx = WireTransaction(inStates, outStates.map { it.state }, commands)
return WireTransaction(inStates, outStates.map { it.state }, wireCmds).toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256()) return wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256())
} }
} }
@ -291,8 +297,8 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
fun <C : ContractState> lookup(label: String) = StateAndRef(label.output as C, label.outputRef) fun <C : ContractState> lookup(label: String) = StateAndRef(label.output as C, label.outputRef)
private inner class InternalLedgerTransactionDSL : LedgerTransactionDSL() { private inner class InternalLedgerTransactionDSL : LedgerTransactionDSL() {
fun finaliseAndInsertLabels(time: Instant): LedgerTransaction { fun finaliseAndInsertLabels(): LedgerTransaction {
val ltx = toLedgerTransaction(time) val ltx = toLedgerTransaction()
for ((index, labelledState) in outStates.withIndex()) { for ((index, labelledState) in outStates.withIndex()) {
if (labelledState.label != null) { if (labelledState.label != null) {
labelToRefs[labelledState.label] = ContractStateRef(ltx.hash, index) labelToRefs[labelledState.label] = ContractStateRef(ltx.hash, index)
@ -317,7 +323,7 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
fun transaction(vararg outputStates: LabeledOutput) { fun transaction(vararg outputStates: LabeledOutput) {
val outs = outputStates.map { it.state } val outs = outputStates.map { it.state }
val wtx = WireTransaction(emptyList(), outs, emptyList()) val wtx = WireTransaction(emptyList(), outs, emptyList())
val ltx = wtx.toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256()) val ltx = wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256())
for ((index, state) in outputStates.withIndex()) { for ((index, state) in outputStates.withIndex()) {
val label = state.label!! val label = state.label!!
labelToRefs[label] = ContractStateRef(ltx.hash, index) labelToRefs[label] = ContractStateRef(ltx.hash, index)
@ -330,17 +336,17 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR) @Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
fun roots(body: Roots.() -> Unit) {} fun roots(body: Roots.() -> Unit) {}
@Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR) @Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR)
fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionDSL.() -> Unit) {} fun transaction(body: LedgerTransactionDSL.() -> Unit) {}
} }
fun roots(body: Roots.() -> Unit) = Roots().apply { body() } fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
val txns = ArrayList<LedgerTransaction>() val txns = ArrayList<LedgerTransaction>()
private val txnToLabelMap = HashMap<LedgerTransaction, String>() private val txnToLabelMap = HashMap<LedgerTransaction, String>()
fun transaction(label: String? = null, time: Instant = TEST_TX_TIME, body: LedgerTransactionDSL.() -> Unit): LedgerTransaction { fun transaction(label: String? = null, body: LedgerTransactionDSL.() -> Unit): LedgerTransaction {
val forTest = InternalLedgerTransactionDSL() val forTest = InternalLedgerTransactionDSL()
forTest.body() forTest.body()
val ltx = forTest.finaliseAndInsertLabels(time) val ltx = forTest.finaliseAndInsertLabels()
txns.add(ltx) txns.add(ltx)
if (label != null) if (label != null)
txnToLabelMap[ltx] = label txnToLabelMap[ltx] = label

View File

@ -8,7 +8,7 @@
package core.visualiser package core.visualiser
import core.Command import core.CommandData
import core.ContractState import core.ContractState
import core.SecureHash import core.SecureHash
import core.testutils.TransactionGroupDSL import core.testutils.TransactionGroupDSL
@ -67,7 +67,7 @@ class GraphVisualiser(val dsl: TransactionGroupDSL<in ContractState>) {
return dsl.labelForState(state) ?: stateToTypeName(state) return dsl.labelForState(state) ?: stateToTypeName(state)
} }
private fun commandToTypeName(state: Command) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.') private fun commandToTypeName(state: CommandData) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.')
private fun stateToTypeName(state: ContractState) = state.javaClass.canonicalName.removePrefix("contracts.").removeSuffix(".State") private fun stateToTypeName(state: ContractState) = state.javaClass.canonicalName.removePrefix("contracts.").removeSuffix(".State")
private fun stateToCSSClass(state: ContractState) = stateToTypeName(state).replace('.', '_').toLowerCase() private fun stateToCSSClass(state: ContractState) = stateToTypeName(state).replace('.', '_').toLowerCase()