diff --git a/docs/source/overview.rst b/docs/source/overview.rst index 84fc640677..c7e0dc0742 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -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 **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 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. @@ -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 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 -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 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. -A transaction has one or more **signatures** attached to it. The signatures do not mean anything by themselves, rather, -their existence is given as input to the contract which can then decide which set of signatures it demands (if any). -Signatures may be from an arbitrary, random **public key** that has no identity attached. A public key may be -well known, that is, appears in some sort of public identity registry. In this case we say the key is owned by a -**party**, which is defined (for now) as being merely a (public key, name) pair. - -A transaction may also be **timestamped**. A timestamp is a (hash, datetime, signature) triple from a -*timestamping authority* (TSA). The notion of a TSA is not ledger specific and is defined by -`IETF RFC 3161 `_ 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. +Transactions may sometimes need to provide a contract with data from the outside world. Examples may include stock +prices, facts about events or the statuses of legal entities (e.g. bankruptcy), and so on. The providers of such +facts are called **oracles** and they provide facts to the ledger by signing transactions that contain commands they +recognise. The commands contain the fact and the signature shows agreement to that fact. Time is also modelled as +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 +time" (i.e. GPS time as calibrated to the US Naval Observatory). 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 diff --git a/src/main/kotlin/contracts/Cash.kt b/src/main/kotlin/contracts/Cash.kt index a34aba3ca1..aad70dbb54 100644 --- a/src/main/kotlin/contracts/Cash.kt +++ b/src/main/kotlin/contracts/Cash.kt @@ -66,8 +66,8 @@ class Cash : Contract { } // Just for grouping - interface Commands : Command { - class Move() : TypeOnlyCommand(), Commands + interface Commands : CommandData { + class Move() : TypeOnlyCommandData(), Commands /** * 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.outputStates().sumCashOrNull() == null) 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) // What if we already have a move command with the right keys? Filter it out here or in platform code? val keysList = keysUsed.toList() - tx.addArg(WireCommand(Commands.Move(), keysList)) + tx.addCommand(Commands.Move(), keysList) return keysList } } diff --git a/src/main/kotlin/contracts/CommercialPaper.kt b/src/main/kotlin/contracts/CommercialPaper.kt index fa031a1d38..2a4eda81bc 100644 --- a/src/main/kotlin/contracts/CommercialPaper.kt +++ b/src/main/kotlin/contracts/CommercialPaper.kt @@ -53,12 +53,12 @@ class CommercialPaper : Contract { override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) } - interface Commands : Command { - class Move : TypeOnlyCommand(), Commands - class Redeem : TypeOnlyCommand(), Commands + interface Commands : CommandData { + class Move : TypeOnlyCommandData(), 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. // 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) { @@ -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 // it for cash on or after the maturity date. val command = tx.commands.requireSingleCommand() - val time = tx.time + val timestamp: TimestampCommand? = tx.getTimestampBy(DummyTimestampingAuthority.identity) for (group in groups) { when (command.value) { @@ -83,7 +83,7 @@ class CommercialPaper : Contract { is Commands.Redeem -> { val input = group.inputs.single() 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 { "the paper must have matured" by (time > input.maturityDate) "the received amount equals the face value" by (received == input.faceValue) @@ -94,7 +94,7 @@ class CommercialPaper : Contract { is Commands.Issue -> { 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 { // Don't allow people to issue commercial paper under other entities identities. "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 { 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, newOwner: PublicKey) { tx.addInputState(paper.ref) 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. Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet) tx.addInputState(paper.ref) - tx.addArg(WireCommand(CommercialPaper.Commands.Redeem(), paper.state.owner)) + tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner) } } diff --git a/src/main/kotlin/contracts/CrowdFund.kt b/src/main/kotlin/contracts/CrowdFund.kt index 2d67dbb01b..4985350264 100644 --- a/src/main/kotlin/contracts/CrowdFund.kt +++ b/src/main/kotlin/contracts/CrowdFund.kt @@ -66,10 +66,10 @@ class CrowdFund : Contract { ) - interface Commands : Command { - class Register : TypeOnlyCommand(), Commands - class Pledge : TypeOnlyCommand(), Commands - class Close : TypeOnlyCommand(), Commands + interface Commands : CommandData { + class Register : TypeOnlyCommandData(), Commands + class Pledge : TypeOnlyCommandData(), Commands + class Close : TypeOnlyCommandData(), Commands } override fun verify(tx: TransactionForVerification) { @@ -79,7 +79,9 @@ class CrowdFund : Contract { val command = tx.commands.requireSingleCommand() val outputCrowdFund: CrowdFund.State = tx.outStates.filterIsInstance().single() val outputCash: List = tx.outStates.filterIsInstance() - val time = tx.time + + val time = tx.getTimestampBy(DummyTimestampingAuthority.identity)?.midpoint + if (time == null) throw IllegalArgumentException("must be timestamped") when (command.value) { is Commands.Register -> { @@ -89,7 +91,7 @@ class CrowdFund : Contract { "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 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) } } @@ -97,7 +99,6 @@ class CrowdFund : Contract { is Commands.Pledge -> { val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance().single() val pledgedCash = outputCash.sumCashBy(inputCrowdFund.campaign.owner) - if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped") requireThat { "campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign) "the campaign is still open" by (inputCrowdFund.campaign.closingTime >= time) @@ -117,8 +118,6 @@ class CrowdFund : Contract { return true } - if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped") - requireThat { "campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign) "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 { val campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime) 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( 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, wallet: List>) { tx.addInputState(campaign.ref) 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.state.pledgedAmount < campaign.state.campaign.target) { for (pledge in campaign.state.pledges) { diff --git a/src/main/kotlin/contracts/JavaCommercialPaper.java b/src/main/kotlin/contracts/JavaCommercialPaper.java index 8a0077ee0e..3a7a0027ca 100644 --- a/src/main/kotlin/contracts/JavaCommercialPaper.java +++ b/src/main/kotlin/contracts/JavaCommercialPaper.java @@ -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 { @Override public boolean equals(Object obj) { @@ -114,9 +114,12 @@ public class JavaCommercialPaper implements Contract { List> groups = tx.groupStates(State.class, State::withoutOwner); // Find the command that instructs us what to do and check there's exactly one. - AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.class); + AuthenticatedObject 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 group : groups) { List 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"); } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) { Amount received = CashKt.sumCashOrNull(inputs); - if (time == null) - throw new IllegalArgumentException("Redemption transactions must be timestamped"); if (received == null) throw new IllegalStateException("Failed requirement: no cash being redeemed"); if (input.getMaturityDate().isAfter(time)) diff --git a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt index 64e9e392b3..940bf83d6c 100644 --- a/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt +++ b/src/main/kotlin/contracts/protocols/TwoPartyTradeProtocol.kt @@ -58,8 +58,8 @@ abstract class TwoPartyTradeProtocol { abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer - abstract class Buyer : ProtocolStateMachine>() - abstract class Seller : ProtocolStateMachine>() + abstract class Buyer : ProtocolStateMachine>() + abstract class Seller : ProtocolStateMachine>() companion object { @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 // learn more about the protocol state machine framework. class SellerImpl : Seller() { - override fun call(args: SellerInitialArgs): Pair { + override fun call(args: SellerInitialArgs): Pair { val sessionID = random63BitValue() // 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" } partialTX.verifySignatures() - val wtx = partialTX.txBits.deserialize() + val wtx: WireTransaction = partialTX.txBits.deserialize() requireThat { "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. } - val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits.bits) + val ourSignature = args.myKeyPair.signWithECDSA(partialTX.txBits) val fullySigned: SignedWireTransaction = partialTX.copy(sigs = partialTX.sigs + ourSignature) // We should run it through our full TransactionGroup of all transactions here. fullySigned.verify() - val timestamped: TimestampedWireTransaction = fullySigned.toTimestampedTransaction(serviceHub.timestampingService) 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. class BuyerImpl : Buyer() { - override fun call(args: BuyerInitialArgs): Pair { + override fun call(args: BuyerInitialArgs): Pair { // Wait for a trade request to come in on our pre-provided session ID. val tradeRequest = receive(TRADE_TOPIC, args.sessionID) @@ -167,7 +166,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) : 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)) + ptx.addCommand(command, tradeRequest.assetForSale.state.owner) // Now sign the transaction with whatever keys we need to move the cash. 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 a malicious buyer sending us back a different transaction to the one we built. - val fullySigned = sendAndReceive(TRADE_TOPIC, - tradeRequest.sessionID, args.sessionID, stx) + val fullySigned = sendAndReceive(TRADE_TOPIC, tradeRequest.sessionID, args.sessionID, stx) 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! :-)" } - return Pair(fullySigned, ltx) + return Pair(fullySigned.verify(), ltx) } } diff --git a/src/main/kotlin/core/ContractsDSL.kt b/src/main/kotlin/core/ContractsDSL.kt index 1b077e44d5..131bc8d20f 100644 --- a/src/main/kotlin/core/ContractsDSL.kt +++ b/src/main/kotlin/core/ContractsDSL.kt @@ -106,17 +106,24 @@ fun Iterable.sumOrZero(currency: Currency) = if (iterator().hasNext()) s //// Authenticated commands /////////////////////////////////////////////////////////////////////////////////////////// /** Filters the command list by type, party and public key all at once. */ -inline fun List>.select(signer: PublicKey? = null, party: Party? = null) = +inline fun List>.select(signer: PublicKey? = null, party: Party? = null) = filter { it.value is T }. filter { if (signer == null) true else it.signers.contains(signer) }. filter { if (party == null) true else it.signingParties.contains(party) }. map { AuthenticatedObject(it.signers, it.signingParties, it.value as T) } -inline fun List>.requireSingleCommand() = try { +inline fun List>.requireSingleCommand() = try { select().single() } catch (e: NoSuchElementException) { throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message. } // For Java -fun List>.requireSingleCommand(klass: Class) = filter { klass.isInstance(it) }.single() +fun List>.requireSingleCommand(klass: Class) = filter { klass.isInstance(it) }.single() + +/** Returns a timestamp that was signed by the given authority, or returns null if missing. */ +fun List>.getTimestampBy(timestampingAuthority: Party): TimestampCommand? { + val timestampCmds = filter { it.signers.contains(timestampingAuthority.owningKey) && it.value is TimestampCommand } + return timestampCmds.singleOrNull()?.value as? TimestampCommand +} + diff --git a/src/main/kotlin/core/Crypto.kt b/src/main/kotlin/core/Crypto.kt index 0ee8bbb84d..1628096467 100644 --- a/src/main/kotlin/core/Crypto.kt +++ b/src/main/kotlin/core/Crypto.kt @@ -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) - } object NullPublicKey : PublicKey, Comparable { @@ -90,8 +89,16 @@ fun PrivateKey.signWithECDSA(bits: ByteArray): DigitalSignature { return DigitalSignature(sig) } -fun PrivateKey.signWithECDSA(bits: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, signWithECDSA(bits).bits) -fun KeyPair.signWithECDSA(bits: ByteArray) = private.signWithECDSA(bits, public) +fun PrivateKey.signWithECDSA(bitsToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { + 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 */ fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) { diff --git a/src/main/kotlin/core/Services.kt b/src/main/kotlin/core/Services.kt index 4160565aba..eca9519f7a 100644 --- a/src/main/kotlin/core/Services.kt +++ b/src/main/kotlin/core/Services.kt @@ -9,10 +9,11 @@ package core import core.messaging.MessagingService +import core.serialization.SerializedBytes import java.security.KeyPair +import java.security.KeyPairGenerator import java.security.PrivateKey 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 @@ -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 - * 'timestamping' in the block chain sense, but rather, implies a semi-trusted third party taking a reading of the - * current time, typically from an atomic clock, and then digitally signing (current time, hash) to produce a timestamp - * triple (signature, time, hash). The purpose of these timestamps is to locate a transaction in the timeline, which is - * important in the absence of blocks. Here we model the timestamp as an opaque byte array. + * Simple interface (for testing) to an abstract timestamping service. Note that this is not "timestamping" in the + * blockchain sense of a total ordering of transactions, but rather, a signature from a well known/trusted timestamping + * service over a transaction that indicates the timestamp in it is accurate. Such a signature may not always be + * necessary: if there are multiple parties involved in a transaction then they can cross-check the timestamp + * themselves. */ interface TimestamperService { - fun timestamp(hash: SecureHash): ByteArray - fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant + fun timestamp(wtxBytes: SerializedBytes): DigitalSignature.LegallyIdentifiable + + /** 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) } /** diff --git a/src/main/kotlin/core/Structures.kt b/src/main/kotlin/core/Structures.kt index 3a9cf383ba..a526dcd1fa 100644 --- a/src/main/kotlin/core/Structures.kt +++ b/src/main/kotlin/core/Structures.kt @@ -11,6 +11,8 @@ package core import core.serialization.OpaqueBytes import core.serialization.serialize 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 @@ -31,7 +33,7 @@ interface OwnableState : ContractState { val owner: PublicKey /** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */ - fun withNewOwner(newOwner: PublicKey): Pair + fun withNewOwner(newOwner: PublicKey): Pair } /** 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 */ -interface Command +interface CommandData /** 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 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) { + 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. */ data class AuthenticatedObject( val signers: List, @@ -79,6 +86,23 @@ data class AuthenticatedObject( 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 * every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the diff --git a/src/main/kotlin/core/TransactionGroup.kt b/src/main/kotlin/core/TransactionGroup.kt index 6f5d250b8a..3806e7910b 100644 --- a/src/main/kotlin/core/TransactionGroup.kt +++ b/src/main/kotlin/core/TransactionGroup.kt @@ -49,7 +49,7 @@ class TransactionGroup(val transactions: Set, val nonVerified // Look up the output in that transaction by 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) diff --git a/src/main/kotlin/core/Transactions.kt b/src/main/kotlin/core/Transactions.kt index 57e1c3c887..936bb07bad 100644 --- a/src/main/kotlin/core/Transactions.kt +++ b/src/main/kotlin/core/Transactions.kt @@ -8,10 +8,14 @@ package core -import core.serialization.* +import core.serialization.SerializedBytes +import core.serialization.deserialize +import core.serialization.serialize import java.security.KeyPair import java.security.PublicKey import java.security.SignatureException +import java.time.Clock +import java.time.Duration import java.time.Instant 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 * 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 * 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 * 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 - * TimestampedWireTransaction i.e. the outermost serialised form with everything included. + * keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of a SignedWireTransaction + * 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 * 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 * multiple parties. * - * LedgerTransaction is derived from WireTransaction and TimestampedWireTransaction together. It is the result of - * doing some basic key lookups on WireCommand to see if any keys are from a recognised party, thus converting - * the WireCommand objects into AuthenticatedObject. Currently we just assume a hard coded pubkey->party - * map. In future it'd make more sense to use a certificate scheme and so that logic would get more complex. + * LedgerTransaction is derived from WireTransaction. It is the result of doing some basic key lookups on WireCommand + * to see if any keys are from a recognised party, thus converting the WireCommand objects into + * AuthenticatedObject. Currently we just assume a hard coded pubkey->party map. In future it'd make more + * 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. * @@ -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. */ -/** 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) { - constructor(command: Command, key: PublicKey) : this(command, listOf(key)) -} - /** Transaction ready for serialisation, without any signatures attached. */ data class WireTransaction(val inputStates: List, val outputStates: List, - val commands: List) { - fun toLedgerTransaction(timestamp: Instant?, identityService: IdentityService, originalHash: SecureHash): LedgerTransaction { + val commands: List) { + fun toLedgerTransaction(identityService: IdentityService, originalHash: SecureHash): LedgerTransaction { val authenticatedArgs = commands.map { val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) } - AuthenticatedObject(it.pubkeys, institutions, it.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. */ class PartialTransaction(private val inputStates: MutableList = arrayListOf(), private val outputStates: MutableList = arrayListOf(), - private val commands: MutableList = arrayListOf()) { + private val commands: MutableList = 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 */ public fun withItems(vararg items: Any): PartialTransaction { for (t in items) { when (t) { is ContractStateRef -> inputStates.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}") } } @@ -88,16 +111,45 @@ class PartialTransaction(private val inputStates: MutableList 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" } 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 { if (checkSufficientSignatures) { - val requiredKeys = commands.flatMap { it.pubkeys }.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)) } @@ -112,17 +164,20 @@ class PartialTransaction(private val inputStates: MutableList outputStates.add(state) } - fun addArg(arg: WireCommand) { + fun addCommand(arg: Command) { check(currentSigs.isEmpty()) // We should probably merge the lists of pubkeys for identical commands here. commands.add(arg) } + fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys))) + fun addCommand(data: CommandData, keys: List) = addCommand(Command(data, keys)) + // Accessors that yield immutable snapshots. fun inputStates(): List = ArrayList(inputStates) fun outputStates(): List = ArrayList(outputStates) - fun commands(): List = ArrayList(commands) + fun commands(): List = ArrayList(commands) } data class SignedWireTransaction(val txBits: SerializedBytes, val sigs: List) { @@ -155,43 +210,17 @@ data class SignedWireTransaction(val txBits: SerializedBytes, v // unverified. val cmdKeys = wtx.commands.flatMap { it.pubkeys }.toSet() val sigKeys = sigs.map { it.by }.toSet() - if (cmdKeys != sigKeys) - throw SignatureException("Command keys don't match the signatures: $cmdKeys vs $sigKeys") + if (!sigKeys.containsAll(cmdKeys)) + throw SignatureException("Missing signatures on the transaction for: ${cmdKeys - sigKeys}") return wtx } - /** Uses the given timestamper service to calculate a signed timestamp and then returns a wrapper for both */ - fun toTimestampedTransaction(timestamper: TimestamperService): TimestampedWireTransaction { - val bits = serialize() - 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( - /** A serialised SignedWireTransaction */ - val signedWireTXBytes: SerializedBytes, - - /** 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) + /** + * Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call + * [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys. + */ + fun verifyToLedgerTransaction(identityService: IdentityService): LedgerTransaction { + return verify().toLedgerTransaction(identityService, txBits.bits.sha256()) } } @@ -206,12 +235,9 @@ data class LedgerTransaction( /** The states that will be generated by the execution of this transaction. */ val outStates: List, /** Arbitrary data passed to the program of each input state. */ - val commands: List>, - /** The moment the transaction was timestamped for, if a timestamp was present. */ - val time: Instant?, + val commands: List>, /** The hash of the original serialised TimestampedWireTransaction or SignedTransaction */ val hash: SecureHash - // TODO: nLockTime equivalent? ) { @Suppress("UNCHECKED_CAST") fun 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. */ data class TransactionForVerification(val inStates: List, val outStates: List, - val commands: List>, - val time: Instant?, + val commands: List>, val origHash: SecureHash) { override fun hashCode() = origHash.hashCode() override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash @@ -258,6 +284,9 @@ data class TransactionForVerification(val inStates: List, data class InOutGroup(val inputs: List, val outputs: List) + // A shortcut to make IDE auto-completion more intuitive for Java users. + fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority) + // For Java users. fun groupStates(ofType: Class, selector: (T) -> Any): List> { val inputs = inStates.filterIsInstance(ofType) diff --git a/src/test/kotlin/contracts/CashTests.kt b/src/test/kotlin/contracts/CashTests.kt index f7c999caf0..a9b41aa942 100644 --- a/src/test/kotlin/contracts/CashTests.kt +++ b/src/test/kotlin/contracts/CashTests.kt @@ -110,7 +110,7 @@ class CashTests { assertEquals(100.DOLLARS, s.amount) assertEquals(MINI_CORP, s.deposit.party) 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]) } diff --git a/src/test/kotlin/contracts/CommercialPaperTests.kt b/src/test/kotlin/contracts/CommercialPaperTests.kt index a527653346..77ca095c79 100644 --- a/src/test/kotlin/contracts/CommercialPaperTests.kt +++ b/src/test/kotlin/contracts/CommercialPaperTests.kt @@ -11,7 +11,9 @@ package contracts import core.* import core.testutils.* import org.junit.Test +import java.time.Clock import java.time.Instant +import java.time.ZoneOffset import kotlin.test.assertFailsWith import kotlin.test.assertTrue @@ -39,6 +41,7 @@ class CommercialPaperTests { transaction { output { PAPER_1 } arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() } + timestamp(TEST_TX_TIME) } expectFailureOfTx(1, "signed by the claimed issuer") @@ -51,6 +54,7 @@ class CommercialPaperTests { transaction { output { PAPER_1.copy(faceValue = 0.DOLLARS) } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + timestamp(TEST_TX_TIME) } expectFailureOfTx(1, "face value is not zero") @@ -63,12 +67,35 @@ class CommercialPaperTests { transaction { output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + timestamp(TEST_TX_TIME) } 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 fun `issue cannot replace an existing state`() { transactionGroup { @@ -79,6 +106,7 @@ class CommercialPaperTests { input("paper") output { PAPER_1 } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + timestamp(TEST_TX_TIME) } expectFailureOfTx(1, "there is no input state") @@ -96,7 +124,7 @@ class CommercialPaperTests { } fun cashOutputsToWallet(vararg states: Cash.State): Pair>> { - 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)) }) } @@ -104,10 +132,13 @@ class CommercialPaperTests { 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) - ptx.signWith(MINI_CORP_KEY) + val ptx = 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) + timestamp(DUMMY_TIMESTAMPER) + } val stx = ptx.toSignedTransaction() - stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256()) + stx.verifyToLedgerTransaction(MockIdentityService) } val (alicesWalletTX, alicesWallet) = cashOutputsToWallet( @@ -124,7 +155,7 @@ class CommercialPaperTests { ptx.signWith(MINI_CORP_KEY) ptx.signWith(ALICE_KEY) val stx = ptx.toSignedTransaction() - stx.verify().toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256()) + stx.verifyToLedgerTransaction(MockIdentityService) } // Won't be validated. @@ -135,10 +166,12 @@ class CommercialPaperTests { fun makeRedeemTX(time: Instant): LedgerTransaction { val ptx = PartialTransaction() + ptx.setTime(time, DummyTimestampingAuthority.identity, 30.seconds) CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet) ptx.signWith(ALICE_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) @@ -154,8 +187,8 @@ class CommercialPaperTests { // Generate a trade lifecycle with various parameters. fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days, - aliceGetsBack: Amount = 1000.DOLLARS, - destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL { + aliceGetsBack: Amount = 1000.DOLLARS, + destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL { val someProfits = 1200.DOLLARS return transactionGroupFor() { roots { @@ -167,6 +200,7 @@ class CommercialPaperTests { transaction("Issuance") { output("paper") { PAPER_1 } 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, @@ -182,7 +216,7 @@ class CommercialPaperTests { // 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. - transaction("Redemption", redemptionTime) { + transaction("Redemption") { input("alice's paper") input("some profits") @@ -193,6 +227,8 @@ class CommercialPaperTests { arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } arg(ALICE) { CommercialPaper.Commands.Redeem() } + + timestamp(redemptionTime) } } } diff --git a/src/test/kotlin/contracts/CrowdFundTests.kt b/src/test/kotlin/contracts/CrowdFundTests.kt index 866e491b8b..e42e3132cb 100644 --- a/src/test/kotlin/contracts/CrowdFundTests.kt +++ b/src/test/kotlin/contracts/CrowdFundTests.kt @@ -34,6 +34,7 @@ class CrowdFundTests { transaction { output { CF_1 } arg(DUMMY_PUBKEY_1) { CrowdFund.Commands.Register() } + timestamp(TEST_TX_TIME) } expectFailureOfTx(1, "the transaction is signed by the owner of the crowdsourcing") @@ -46,6 +47,7 @@ class CrowdFundTests { transaction { output { CF_1.copy(campaign = CF_1.campaign.copy(closingTime = TEST_TX_TIME - 1.days)) } arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() } + timestamp(TEST_TX_TIME) } expectFailureOfTx(1, "the output registration has a closing time in the future") @@ -67,6 +69,7 @@ class CrowdFundTests { transaction { output("funding opportunity") { CF_1 } arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() } + timestamp(TEST_TX_TIME) } // 2. Place a pledge @@ -81,19 +84,21 @@ class CrowdFundTests { output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY } arg(ALICE) { Cash.Commands.Move() } arg(ALICE) { CrowdFund.Commands.Pledge() } + timestamp(TEST_TX_TIME) } // 3. Close the opportunity, assuming the target has been met - transaction(time = TEST_TX_TIME + 8.days) { + transaction { input ("pledged opportunity") output ("funded and closed") { "pledged opportunity".output.copy(closed = true) } arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Close() } + timestamp(time = TEST_TX_TIME + 8.days) } } } fun cashOutputsToWallet(vararg states: Cash.State): Pair>> { - 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)) }) } @@ -102,10 +107,13 @@ 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) - ptx.signWith(MINI_CORP_KEY) + val ptx = CrowdFund().craftRegister(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) + } 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 @@ -120,10 +128,12 @@ class CrowdFundTests { val ptx = PartialTransaction() CrowdFund().craftPledge(ptx, registerTX.outRef(0), ALICE) Cash().craftSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet) + ptx.setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) ptx.signWith(ALICE_KEY) + ptx.timestamp(DUMMY_TIMESTAMPER) val stx = ptx.toSignedTransaction() // 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. @@ -134,10 +144,12 @@ class CrowdFundTests { // MiniCorp closes their campaign. fun makeFundedTX(time: Instant): LedgerTransaction { val ptx = PartialTransaction() + ptx.setTime(time, DUMMY_TIMESTAMPER.identity, 30.seconds) CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet) ptx.signWith(MINI_CORP_KEY) + ptx.timestamp(DUMMY_TIMESTAMPER) val stx = ptx.toSignedTransaction() - return stx.verify().toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256()) + return stx.verifyToLedgerTransaction(MockIdentityService) } val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days) @@ -150,7 +162,5 @@ class CrowdFundTests { // This verification passes TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(TEST_PROGRAM_MAP) - } - } \ No newline at end of file diff --git a/src/test/kotlin/core/TransactionGroupTests.kt b/src/test/kotlin/core/TransactionGroupTests.kt index 18113f9297..dd3575c37a 100644 --- a/src/test/kotlin/core/TransactionGroupTests.kt +++ b/src/test/kotlin/core/TransactionGroupTests.kt @@ -97,7 +97,7 @@ class TransactionGroupTests { // points nowhere. val ref = ContractStateRef(SecureHash.randomSHA256(), 0) 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) { diff --git a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt index e52c157cd0..add4682705 100644 --- a/src/test/kotlin/core/serialization/TransactionSerializationTests.kt +++ b/src/test/kotlin/core/serialization/TransactionSerializationTests.kt @@ -16,7 +16,6 @@ import org.junit.Test import java.security.SignatureException import kotlin.test.assertEquals import kotlin.test.assertFailsWith -import kotlin.test.assertNull class TransactionSerializationTests { // Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change). @@ -31,7 +30,7 @@ class TransactionSerializationTests { @Before fun setup() { 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. assertFailsWith(SignatureException::class) { 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) signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify() @@ -86,18 +85,14 @@ class TransactionSerializationTests { @Test fun timestamp() { + tx.setTime(TEST_TX_TIME, DUMMY_TIMESTAMPER.identity, 30.seconds) + tx.timestamp(DUMMY_TIMESTAMPER) tx.signWith(TestUtils.keypair) - val ttx = tx.toSignedTransaction().toTimestampedTransactionWithoutTime() - val ltx = ttx.verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService) - assertEquals(tx.commands().map { it.command }, ltx.commands.map { it.value }) + 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) - assertNull(ltx.time) - - val ltx2: LedgerTransaction = tx. - toSignedTransaction(). - toTimestampedTransaction(DUMMY_TIMESTAMPER). - verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService) - assertEquals(TEST_TX_TIME, ltx2.time) + assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_TIMESTAMPER.identity)!!.midpoint) } } \ No newline at end of file diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index d773365002..73e167eef7 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -10,20 +10,20 @@ package core.testutils -import com.google.common.io.BaseEncoding import contracts.* import core.* import core.messaging.MessagingService +import core.serialization.SerializedBytes +import core.serialization.deserialize 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.KeyPairGenerator import java.security.PrivateKey import java.security.PublicKey +import java.time.Clock +import java.time.Duration import java.time.Instant +import java.time.ZoneId import java.util.* import javax.annotation.concurrent.ThreadSafe import kotlin.test.assertEquals @@ -67,27 +67,19 @@ val TEST_PROGRAM_MAP: Map = mapOf( ) /** - * A test/mock timestamping service that doesn't use any signatures or security. It always timestamps with - * [TEST_TX_TIME], an arbitrary point on the timeline. + * A test/mock timestamping service that doesn't use any signatures or security. It timestamps with + * 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 { - override fun timestamp(hash: SecureHash): ByteArray { - val bos = ByteArrayOutputStream() - DataOutputStream(bos).use { - it.writeLong(time.toEpochMilli()) - it.write(hash.bits) - } - return bos.toByteArray() - } +class DummyTimestamper(var clock: Clock = Clock.fixed(TEST_TX_TIME, ZoneId.systemDefault()), + val tolerance: Duration = 30.seconds) : TimestamperService { + override val identity = DummyTimestampingAuthority.identity - override fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant { - val dis = DataInputStream(ByteArrayInputStream(signedTimestamp)) - val epochMillis = dis.readLong() - val serHash = ByteArray(32) - dis.readFully(serHash) - if (!Arrays.equals(serHash, hash.bits)) - throw IllegalStateException("Hash mismatch: ${BaseEncoding.base16().encode(serHash)} vs ${BaseEncoding.base16().encode(hash.bits)}") - return Instant.ofEpochMilli(epochMillis) + override fun timestamp(wtxBytes: SerializedBytes): DigitalSignature.LegallyIdentifiable { + val wtx = wtxBytes.deserialize() + val timestamp = wtx.commands.mapNotNull { it.data as? TimestampCommand }.single() + if (Duration.between(timestamp.before, clock.instant()) > tolerance) + throw NotOnTimeException() + return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity) } } @@ -176,13 +168,26 @@ infix fun ContractState.label(label: String) = LabeledOutput(label, this) abstract class AbstractTransactionForTest { protected val outStates = ArrayList() - protected val commands = ArrayList>() + protected val commands = ArrayList() 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> { + 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) - 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 { ... } } @@ -196,7 +201,8 @@ open class TransactionForTest : AbstractTransactionForTest() { fun input(s: () -> ContractState) = inStates.add(s()) 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) } @@ -276,12 +282,12 @@ class TransactionGroupDSL(private val stateType: Class) { /** - * Converts to a [LedgerTransaction] with the givn time, the test institution map, and just assigns a random - * hash (i.e. pretend it was signed) + * Converts to a [LedgerTransaction] with the test institution map, and just assigns a random hash + * (i.e. pretend it was signed) */ - fun toLedgerTransaction(time: Instant): LedgerTransaction { - val wireCmds = commands.map { WireCommand(it.value, it.signers) } - return WireTransaction(inStates, outStates.map { it.state }, wireCmds).toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256()) + fun toLedgerTransaction(): LedgerTransaction { + val wtx = WireTransaction(inStates, outStates.map { it.state }, commands) + return wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256()) } } @@ -291,8 +297,8 @@ class TransactionGroupDSL(private val stateType: Class) { fun lookup(label: String) = StateAndRef(label.output as C, label.outputRef) private inner class InternalLedgerTransactionDSL : LedgerTransactionDSL() { - fun finaliseAndInsertLabels(time: Instant): LedgerTransaction { - val ltx = toLedgerTransaction(time) + fun finaliseAndInsertLabels(): LedgerTransaction { + val ltx = toLedgerTransaction() for ((index, labelledState) in outStates.withIndex()) { if (labelledState.label != null) { labelToRefs[labelledState.label] = ContractStateRef(ltx.hash, index) @@ -317,7 +323,7 @@ class TransactionGroupDSL(private val stateType: Class) { fun transaction(vararg outputStates: LabeledOutput) { val outs = outputStates.map { it.state } val wtx = WireTransaction(emptyList(), outs, emptyList()) - val ltx = wtx.toLedgerTransaction(TEST_TX_TIME, MockIdentityService, SecureHash.randomSHA256()) + val ltx = wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256()) for ((index, state) in outputStates.withIndex()) { val label = state.label!! labelToRefs[label] = ContractStateRef(ltx.hash, index) @@ -330,17 +336,17 @@ class TransactionGroupDSL(private val stateType: Class) { @Deprecated("Does not nest ", level = DeprecationLevel.ERROR) fun roots(body: Roots.() -> Unit) {} @Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR) - fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionDSL.() -> Unit) {} + fun transaction(body: LedgerTransactionDSL.() -> Unit) {} } fun roots(body: Roots.() -> Unit) = Roots().apply { body() } val txns = ArrayList() private val txnToLabelMap = HashMap() - 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() forTest.body() - val ltx = forTest.finaliseAndInsertLabels(time) + val ltx = forTest.finaliseAndInsertLabels() txns.add(ltx) if (label != null) txnToLabelMap[ltx] = label diff --git a/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt b/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt index f7e34e6ab0..7a3149499d 100644 --- a/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt +++ b/src/test/kotlin/core/visualiser/GroupToGraphConversion.kt @@ -8,7 +8,7 @@ package core.visualiser -import core.Command +import core.CommandData import core.ContractState import core.SecureHash import core.testutils.TransactionGroupDSL @@ -67,7 +67,7 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { 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 stateToCSSClass(state: ContractState) = stateToTypeName(state).replace('.', '_').toLowerCase()