diff --git a/src/main/kotlin/contracts/CommercialPaper.kt b/src/main/kotlin/contracts/CommercialPaper.kt index 2be93fd9e1..2a4eda81bc 100644 --- a/src/main/kotlin/contracts/CommercialPaper.kt +++ b/src/main/kotlin/contracts/CommercialPaper.kt @@ -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 diff --git a/src/main/kotlin/contracts/CrowdFund.kt b/src/main/kotlin/contracts/CrowdFund.kt index 5ab032bca5..4985350264 100644 --- a/src/main/kotlin/contracts/CrowdFund.kt +++ b/src/main/kotlin/contracts/CrowdFund.kt @@ -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) diff --git a/src/main/kotlin/contracts/JavaCommercialPaper.java b/src/main/kotlin/contracts/JavaCommercialPaper.java index 0e56ae291f..3a7a0027ca 100644 --- a/src/main/kotlin/contracts/JavaCommercialPaper.java +++ b/src/main/kotlin/contracts/JavaCommercialPaper.java @@ -116,7 +116,10 @@ public class JavaCommercialPaper implements Contract { // Find the command that instructs us what to do and check there's exactly one. 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 c7e2a34da2..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) @@ -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 aa9ffb4289..131bc8d20f 100644 --- a/src/main/kotlin/core/ContractsDSL.kt +++ b/src/main/kotlin/core/ContractsDSL.kt @@ -120,3 +120,10 @@ inline fun List>.requ // For Java 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 a6be19faca..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 @@ -84,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 6b31994617..cd713a7557 100644 --- a/src/main/kotlin/core/Transactions.kt +++ b/src/main/kotlin/core/Transactions.kt @@ -8,10 +8,13 @@ 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.Duration import java.time.Instant import java.util.* @@ -19,26 +22,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. * @@ -50,19 +50,38 @@ import java.util.* data class WireTransaction(val inputStates: List, val outputStates: List, val commands: List) { - fun toLedgerTransaction(timestamp: Instant?, identityService: IdentityService, originalHash: SecureHash): LedgerTransaction { + fun toLedgerTransaction(identityService: IdentityService, originalHash: SecureHash): LedgerTransaction { val authenticatedArgs = commands.map { val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) } AuthenticatedObject(it.pubkeys, institutions, it.data) } - return LedgerTransaction(inputStates, outputStates, authenticatedArgs, 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 TooLateException : 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()) { + + 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. + */ + fun setTime(time: Instant, authenticatedBy: Party) { + check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" } + commands.removeAll { it.data is TimestampCommand } + addCommand(TimestampCommand(time, 30.seconds), 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) { @@ -83,16 +102,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) { + // 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(Instant.now(), t.before) > maxExpectedLatency) + throw TooLateException() + + // The timestamper may also throw TooLateException 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)) } @@ -153,43 +201,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()) } } @@ -205,11 +227,8 @@ data class LedgerTransaction( 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?, /** 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)) @@ -222,11 +241,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 origHash: SecureHash) { override fun hashCode() = origHash.hashCode() override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash @@ -256,6 +275,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/CommercialPaperTests.kt b/src/test/kotlin/contracts/CommercialPaperTests.kt index a527653346..b7d9276532 100644 --- a/src/test/kotlin/contracts/CommercialPaperTests.kt +++ b/src/test/kotlin/contracts/CommercialPaperTests.kt @@ -39,6 +39,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 +52,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,6 +65,7 @@ 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") @@ -79,6 +82,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 +100,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 +108,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) + 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 +131,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 +142,12 @@ class CommercialPaperTests { fun makeRedeemTX(time: Instant): LedgerTransaction { val ptx = PartialTransaction() + ptx.setTime(time, DummyTimestampingAuthority.identity ) 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 +163,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 +176,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 +192,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 +203,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..56704f3c0a 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) + 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) 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) 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 529dda2ea4..a96cda885b 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). @@ -86,18 +85,14 @@ class TransactionSerializationTests { @Test fun timestamp() { + tx.setTime(TEST_TX_TIME, DUMMY_TIMESTAMPER.identity) + tx.timestamp(DUMMY_TIMESTAMPER) tx.signWith(TestUtils.keypair) - val ttx = tx.toSignedTransaction().toTimestampedTransactionWithoutTime() - val ltx = ttx.verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService) + 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 e8742337f7..468eb4651a 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 TooLateException() + return DummyTimestampingAuthority.key.signWithECDSA(wtxBytes.bits, identity) } } @@ -189,6 +181,10 @@ abstract class AbstractTransactionForTest { commands.add(Command(c(), keys)) } + fun timestamp(time: Instant) { + commands.add(Command(TimestampCommand(time, 30.seconds), DUMMY_TIMESTAMPER.identity.owningKey)) + } + // Forbid patterns like: transaction { ... transaction { ... } } @Deprecated("Cannot nest transactions, use tweak", level = DeprecationLevel.ERROR) fun transaction(body: TransactionForTest.() -> Unit) {} @@ -200,8 +196,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 }, commandsToAuthenticatedObjects(), - time, SecureHash.randomSHA256()) + val cmds = commandsToAuthenticatedObjects() + val tx = TransactionForVerification(inStates, outStates.map { it.state }, cmds, SecureHash.randomSHA256()) tx.verify(TEST_PROGRAM_MAP) } @@ -281,12 +277,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 { + fun toLedgerTransaction(): LedgerTransaction { val wtx = WireTransaction(inStates, outStates.map { it.state }, commands) - return wtx.toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256()) + return wtx.toLedgerTransaction(MockIdentityService, SecureHash.randomSHA256()) } } @@ -296,8 +292,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) @@ -322,7 +318,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) @@ -335,17 +331,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