Reorganise how time is handled: timestamping authorities are now oracles who sign the entire transaction.

As a result, TimestampedWireTransaction is gone. Timestamp fields are gone. Timestamp commands contain before/after fields. The notion of time tolerance is now a part of the timestamping interface and timestamp data.

TODO:
- Unit tests to verify the notBefore/notAfter logic
- Documentation update
This commit is contained in:
Mike Hearn 2015-12-18 15:27:54 +01:00
parent 6144ccc2c7
commit 784452ac50
15 changed files with 232 additions and 155 deletions

View File

@ -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<CommercialPaper.Commands>()
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

View File

@ -79,7 +79,9 @@ class CrowdFund : Contract {
val command = tx.commands.requireSingleCommand<CrowdFund.Commands>()
val outputCrowdFund: CrowdFund.State = tx.outStates.filterIsInstance<CrowdFund.State>().single()
val outputCash: List<Cash.State> = tx.outStates.filterIsInstance<Cash.State>()
val time = tx.time
val time = tx.getTimestampBy(DummyTimestampingAuthority.identity)?.midpoint
if (time == null) throw IllegalArgumentException("must be timestamped")
when (command.value) {
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<CrowdFund.State>().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)

View File

@ -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<CommandData> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
Instant time = tx.getTime(); // Can be null/missing.
TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity());
if (timestampCommand == null)
throw new IllegalArgumentException("must be timestamped");
Instant time = timestampCommand.getMidpoint();
for (InOutGroup<State> group : groups) {
List<State> inputs = group.getInputs();
@ -138,8 +141,6 @@ public class JavaCommercialPaper implements Contract {
throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner");
} 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))

View File

@ -58,8 +58,8 @@ abstract class TwoPartyTradeProtocol {
abstract fun runBuyer(otherSide: SingleMessageRecipient, args: BuyerInitialArgs): Buyer
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<TimestampedWireTransaction, LedgerTransaction>>()
abstract class Buyer : ProtocolStateMachine<BuyerInitialArgs, Pair<WireTransaction, LedgerTransaction>>()
abstract class Seller : ProtocolStateMachine<SellerInitialArgs, Pair<WireTransaction, LedgerTransaction>>()
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<TimestampedWireTransaction, LedgerTransaction> {
override fun call(args: SellerInitialArgs): Pair<WireTransaction, LedgerTransaction> {
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<WireTransaction>()
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<TimestampedWireTransaction, LedgerTransaction> {
override fun call(args: BuyerInitialArgs): Pair<WireTransaction, LedgerTransaction> {
// Wait for a trade request to come in on our pre-provided session ID.
val tradeRequest = receive<SellerTradeInfo>(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<TimestampedWireTransaction>(TRADE_TOPIC,
tradeRequest.sessionID, args.sessionID, stx)
val fullySigned = sendAndReceive<SignedWireTransaction>(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)
}
}

View File

@ -120,3 +120,10 @@ inline fun <reified T : CommandData> List<AuthenticatedObject<CommandData>>.requ
// For Java
fun List<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<out CommandData>) = filter { klass.isInstance(it) }.single()
/** Returns a timestamp that was signed by the given authority, or returns null if missing. */
fun List<AuthenticatedObject<CommandData>>.getTimestampBy(timestampingAuthority: Party): TimestampCommand? {
val timestampCmds = filter { it.signers.contains(timestampingAuthority.owningKey) && it.value is TimestampCommand }
return timestampCmds.singleOrNull()?.value as? TimestampCommand
}

View File

@ -60,7 +60,6 @@ open class DigitalSignature(bits: ByteArray, val covering: Int = 0) : OpaqueByte
}
class LegallyIdentifiable(val signer: Party, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
}
object NullPublicKey : PublicKey, Comparable<PublicKey> {
@ -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) {

View File

@ -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<WireTransaction>): 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)
}
/**

View File

@ -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<out T : Any>(
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

View File

@ -49,7 +49,7 @@ class TransactionGroup(val transactions: Set<LedgerTransaction>, 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)

View File

@ -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<Command>. 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<Command>. 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<ContractStateRef>,
val outputStates: List<ContractState>,
val commands: List<Command>) {
fun toLedgerTransaction(timestamp: Instant?, identityService: IdentityService, originalHash: SecureHash): LedgerTransaction {
fun toLedgerTransaction(identityService: IdentityService, originalHash: SecureHash): LedgerTransaction {
val authenticatedArgs = commands.map {
val 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<ContractStateRef> = arrayListOf(),
private val outputStates: MutableList<ContractState> = arrayListOf(),
private val commands: MutableList<Command> = arrayListOf()) {
val time: TimestampCommand? get() = commands.mapNotNull { it.data as? TimestampCommand }.singleOrNull()
/**
* Places a [TimestampCommand] in this transaction, removing any existing command if there is one.
* To get the right signature from the timestamping service, use the [timestamp] method.
*/
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<ContractStateRef>
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<WireTransaction>, 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<SignedWireTransaction>,
/** Signature from a timestamping authority. For instance using RFC 3161 */
val timestamp: OpaqueBytes?
) {
val transactionID: SecureHash = serialize().sha256()
fun verifyToLedgerTransaction(timestamper: TimestamperService, identityService: IdentityService): LedgerTransaction {
val stx = signedWireTXBytes.deserialize()
val wtx: WireTransaction = stx.verify()
val instant: Instant? =
if (timestamp != null)
timestamper.verifyTimestamp(signedWireTXBytes.sha256(), timestamp.bits)
else
null
return wtx.toLedgerTransaction(instant, identityService, transactionID)
/**
* 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<ContractState>,
/** Arbitrary data passed to the program of each input state. */
val commands: List<AuthenticatedObject<CommandData>>,
/** The moment the transaction was timestamped for, if a timestamp was present. */
val time: Instant?,
/** The hash of the original serialised TimestampedWireTransaction or SignedTransaction */
val hash: SecureHash
// TODO: nLockTime equivalent?
) {
@Suppress("UNCHECKED_CAST")
fun <T : ContractState> 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<ContractState>,
val outStates: List<ContractState>,
val commands: List<AuthenticatedObject<CommandData>>,
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<ContractState>,
data class InOutGroup<T : ContractState>(val inputs: List<T>, val outputs: List<T>)
// A shortcut to make IDE auto-completion more intuitive for Java users.
fun getTimestampBy(timestampingAuthority: Party): TimestampCommand? = commands.getTimestampBy(timestampingAuthority)
// For Java users.
fun <T : ContractState> groupStates(ofType: Class<T>, selector: (T) -> Any): List<InOutGroup<T>> {
val inputs = inStates.filterIsInstance(ofType)

View File

@ -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<LedgerTransaction, List<StateAndRef<Cash.State>>> {
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), TEST_TX_TIME, SecureHash.randomSHA256())
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, ContractStateRef(ltx.hash, index)) })
}
@ -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<CommercialPaper.State> {
aliceGetsBack: Amount = 1000.DOLLARS,
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<CommercialPaper.State> {
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)
}
}
}

View File

@ -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<LedgerTransaction, List<StateAndRef<Cash.State>>> {
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), TEST_TX_TIME, SecureHash.randomSHA256())
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, ContractStateRef(ltx.hash, index)) })
}
@ -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)
}
}

View File

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

View File

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

View File

@ -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<SecureHash, Contract> = 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<WireTransaction>): 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<T : ContractState>(private val stateType: Class<T>) {
/**
* 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<T : ContractState>(private val stateType: Class<T>) {
fun <C : ContractState> 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<T : ContractState>(private val stateType: Class<T>) {
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<T : ContractState>(private val stateType: Class<T>) {
@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<LedgerTransaction>()
private val txnToLabelMap = HashMap<LedgerTransaction, String>()
fun transaction(label: String? = null, time: Instant = TEST_TX_TIME, body: LedgerTransactionDSL.() -> Unit): LedgerTransaction {
fun transaction(label: String? = null, body: LedgerTransactionDSL.() -> Unit): LedgerTransaction {
val forTest = InternalLedgerTransactionDSL()
forTest.body()
val ltx = forTest.finaliseAndInsertLabels(time)
val ltx = forTest.finaliseAndInsertLabels()
txns.add(ltx)
if (label != null)
txnToLabelMap[ltx] = label