diff --git a/docs/build/html/_sources/tutorial.txt b/docs/build/html/_sources/tutorial.txt
index d30268c2b8..29d50207ba 100644
--- a/docs/build/html/_sources/tutorial.txt
+++ b/docs/build/html/_sources/tutorial.txt
@@ -320,6 +320,7 @@ logic.
.. sourcecode:: kotlin
+ val time = tx.time
for (group in groups) {
when (command.value) {
is Commands.Move -> {
@@ -333,8 +334,9 @@ logic.
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")
requireThat {
- "the paper must have matured" by (input.maturityDate < tx.time)
+ "the paper must have matured" by (time > input.maturityDate)
"the received amount equals the face value" by (received == input.faceValue)
"the paper must be destroyed" by group.outputs.isEmpty()
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
@@ -343,12 +345,13 @@ logic.
is Commands.Issue -> {
val output = group.outputs.single()
+ if (time == null) throw IllegalArgumentException("Issuance transactions 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
(command.signers.contains(output.issuance.institution.owningKey))
"the face value is not zero" by (output.faceValue.pennies > 0)
- "the maturity date is not in the past" by (output.maturityDate > tx.time)
+ "the maturity date is not in the past" by (time < output.maturityDate )
// Don't allow an existing CP state to be replaced by this issuance.
"there is no input state" by group.inputs.isEmpty()
}
@@ -361,6 +364,7 @@ logic.
.. sourcecode:: java
+ Instant time = tx.getTime(); // Can be null/missing.
for (InOutGroup
After extracting the command and the groups, we then iterate over each group and verify it meets the required business logic.
for (group in groups) {
+val time = tx.time
+for (group in groups) {
when (command.value) {
is Commands.Move -> {
val input = group.inputs.single()
@@ -431,8 +432,9 @@ logic.
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")
requireThat {
- "the paper must have matured" by (input.maturityDate < tx.time)
+ "the paper must have matured" by (time > input.maturityDate)
"the received amount equals the face value" by (received == input.faceValue)
"the paper must be destroyed" by group.outputs.isEmpty()
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
@@ -441,12 +443,13 @@ logic.
is Commands.Issue -> {
val output = group.outputs.single()
+ if (time == null) throw IllegalArgumentException("Issuance transactions 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
(command.signers.contains(output.issuance.institution.owningKey))
"the face value is not zero" by (output.faceValue.pennies > 0)
- "the maturity date is not in the past" by (output.maturityDate > tx.time)
+ "the maturity date is not in the past" by (time < output.maturityDate )
// Don't allow an existing CP state to be replaced by this issuance.
"there is no input state" by group.inputs.isEmpty()
}
@@ -458,7 +461,8 @@ logic.
}
-for (InOutGroup<State> group : groups) {
+Instant time = tx.getTime(); // Can be null/missing.
+for (InOutGroup<State> group : groups) {
List<State> inputs = group.getInputs();
List<State> outputs = group.getOutputs();
@@ -478,9 +482,11 @@ logic.
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(tx.getTime()))
+ if (input.getMaturityDate().isAfter(time))
throw new IllegalStateException("Failed requirement: the paper must have matured");
if (!input.getFaceValue().equals(received))
throw new IllegalStateException("Failed requirement: the received amount equals the face value");
@@ -494,6 +500,15 @@ logic.
This loop is the core logic of the contract.
+The first line simply gets the timestamp out of the transaction. Timestamping of transactions is optional, so a time
+may be missing here. We check for it being null later.
+
+Note
+In the Kotlin version, as long as we write a comparison with the transaction time first, the compiler will
+verify we didn’t forget to check if it’s missing. Unfortunately due to the need for smooth Java interop, this
+check won’t happen if we write e.g. someDate > time, it has to be time < someDate. So it’s good practice to
+always write the transaction timestamp first.
+
The first line (first three lines in Java) impose a requirement that there be a single piece of commercial paper in
this group. We do not allow multiple units of CP to be split or merged even if they are owned by the same owner. The
single()
method is a static extension method defined by the Kotlin standard library: given a list, it throws an
diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst
index d30268c2b8..17926030e6 100644
--- a/docs/source/tutorial.rst
+++ b/docs/source/tutorial.rst
@@ -320,6 +320,7 @@ logic.
.. sourcecode:: kotlin
+ val time = tx.time
for (group in groups) {
when (command.value) {
is Commands.Move -> {
@@ -333,8 +334,9 @@ logic.
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")
requireThat {
- "the paper must have matured" by (input.maturityDate < tx.time)
+ "the paper must have matured" by (time > input.maturityDate)
"the received amount equals the face value" by (received == input.faceValue)
"the paper must be destroyed" by group.outputs.isEmpty()
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
@@ -343,12 +345,13 @@ logic.
is Commands.Issue -> {
val output = group.outputs.single()
+ if (time == null) throw IllegalArgumentException("Issuance transactions 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
(command.signers.contains(output.issuance.institution.owningKey))
"the face value is not zero" by (output.faceValue.pennies > 0)
- "the maturity date is not in the past" by (output.maturityDate > tx.time)
+ "the maturity date is not in the past" by (time < output.maturityDate )
// Don't allow an existing CP state to be replaced by this issuance.
"there is no input state" by group.inputs.isEmpty()
}
@@ -361,6 +364,7 @@ logic.
.. sourcecode:: java
+ Instant time = tx.getTime(); // Can be null/missing.
for (InOutGroup group : groups) {
List inputs = group.getInputs();
List outputs = group.getOutputs();
@@ -381,9 +385,11 @@ logic.
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(tx.getTime()))
+ if (input.getMaturityDate().isAfter(time))
throw new IllegalStateException("Failed requirement: the paper must have matured");
if (!input.getFaceValue().equals(received))
throw new IllegalStateException("Failed requirement: the received amount equals the face value");
@@ -396,6 +402,14 @@ logic.
This loop is the core logic of the contract.
+The first line simply gets the timestamp out of the transaction. Timestamping of transactions is optional, so a time
+may be missing here. We check for it being null later.
+
+.. note:: In the Kotlin version, as long as we write a comparison with the transaction time first, the compiler will
+ verify we didn't forget to check if it's missing. Unfortunately due to the need for smooth Java interop, this
+ check won't happen if we write e.g. ``someDate > time``, it has to be ``time < someDate``. So it's good practice to
+ always write the transaction timestamp first.
+
The first line (first three lines in Java) impose a requirement that there be a single piece of commercial paper in
this group. We do not allow multiple units of CP to be split or merged even if they are owned by the same owner. The
``single()`` method is a static *extension method* defined by the Kotlin standard library: given a list, it throws an
diff --git a/src/contracts/Cash.kt b/src/contracts/Cash.kt
index d5d5c4edc1..4f49334621 100644
--- a/src/contracts/Cash.kt
+++ b/src/contracts/Cash.kt
@@ -56,7 +56,7 @@ class Cash : Contract {
// Just for grouping
interface Commands : Command {
- object Move : Commands
+ class Move() : TypeOnlyCommand(), Commands
/**
* Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
@@ -210,7 +210,7 @@ class Cash : Contract {
for (state in gathered) tx.addInputState(state.ref)
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?
- tx.addArg(WireCommand(Commands.Move, keysUsed.toList()))
+ tx.addArg(WireCommand(Commands.Move(), keysUsed.toList()))
}
}
diff --git a/src/contracts/CommercialPaper.kt b/src/contracts/CommercialPaper.kt
index fbc76c76b4..b542f10cec 100644
--- a/src/contracts/CommercialPaper.kt
+++ b/src/contracts/CommercialPaper.kt
@@ -40,11 +40,11 @@ class CommercialPaper : Contract {
}
interface Commands : Command {
- object Move : Commands
- object Redeem : Commands
+ class Move : TypeOnlyCommand(), Commands
+ class Redeem : TypeOnlyCommand(), 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.
- object Issue : Commands
+ class Issue : TypeOnlyCommand(), Commands
}
override fun verify(tx: TransactionForVerification) {
@@ -54,6 +54,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
for (group in groups) {
when (command.value) {
@@ -68,8 +69,9 @@ 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")
requireThat {
- "the paper must have matured" by (input.maturityDate < tx.time)
+ "the paper must have matured" by (time > input.maturityDate)
"the received amount equals the face value" by (received == input.faceValue)
"the paper must be destroyed" by group.outputs.isEmpty()
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
@@ -78,12 +80,13 @@ class CommercialPaper : Contract {
is Commands.Issue -> {
val output = group.outputs.single()
+ if (time == null) throw IllegalArgumentException("Redemption transactions 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
(command.signers.contains(output.issuance.institution.owningKey))
"the face value is not zero" by (output.faceValue.pennies > 0)
- "the maturity date is not in the past" by (output.maturityDate > tx.time)
+ "the maturity date is not in the past" by (time < output.maturityDate)
// Don't allow an existing CP state to be replaced by this issuance.
"there is no input state" by group.inputs.isEmpty()
}
@@ -102,7 +105,7 @@ class CommercialPaper : Contract {
*/
fun craftIssue(issuance: InstitutionReference, faceValue: Amount, maturityDate: Instant): PartialTransaction {
val state = State(issuance, issuance.institution.owningKey, faceValue, maturityDate)
- return PartialTransaction(state, WireCommand(Commands.Issue, issuance.institution.owningKey))
+ return PartialTransaction(state, WireCommand(Commands.Issue(), issuance.institution.owningKey))
}
/**
@@ -111,7 +114,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.addArg(WireCommand(Commands.Move(), paper.state.owner))
}
/**
@@ -126,7 +129,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.addArg(WireCommand(CommercialPaper.Commands.Redeem(), paper.state.owner))
}
}
diff --git a/src/contracts/JavaCommercialPaper.java b/src/contracts/JavaCommercialPaper.java
index 814c5f11a7..cb622c986d 100644
--- a/src/contracts/JavaCommercialPaper.java
+++ b/src/contracts/JavaCommercialPaper.java
@@ -109,6 +109,8 @@ 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.
+
for (InOutGroup group : groups) {
List inputs = group.getInputs();
List outputs = group.getOutputs();
@@ -129,9 +131,11 @@ 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(tx.getTime()))
+ if (input.getMaturityDate().isAfter(time))
throw new IllegalStateException("Failed requirement: the paper must have matured");
if (!input.getFaceValue().equals(received))
throw new IllegalStateException("Failed requirement: the received amount equals the face value");
diff --git a/src/core/Crypto.kt b/src/core/Crypto.kt
index fb000511b4..26ad4ca8b0 100644
--- a/src/core/Crypto.kt
+++ b/src/core/Crypto.kt
@@ -63,6 +63,8 @@ class DummyPublicKey(val s: String) : PublicKey, Comparable, Serializ
override fun getEncoded() = s.toByteArray()
override fun getFormat() = "ASN.1"
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
+ override fun equals(other: Any?) = other is DummyPublicKey && other.s == s
+ override fun hashCode(): Int = s.hashCode()
override fun toString() = "PUBKEY[$s]"
}
diff --git a/src/core/Structures.kt b/src/core/Structures.kt
index c3b6c9738f..8f9cbcfd55 100644
--- a/src/core/Structures.kt
+++ b/src/core/Structures.kt
@@ -49,6 +49,12 @@ data class InstitutionReference(val institution: Institution, val reference: Opa
/** Marker interface for classes that represent commands */
interface Command : SerializeableWithKryo
+/** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */
+abstract class TypeOnlyCommand : Command {
+ override fun equals(other: Any?) = other?.javaClass == javaClass
+ override fun hashCode() = javaClass.name.hashCode()
+}
+
/** 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,
diff --git a/src/core/Transactions.kt b/src/core/Transactions.kt
index 435efae491..1c07c6ff58 100644
--- a/src/core/Transactions.kt
+++ b/src/core/Transactions.kt
@@ -51,7 +51,7 @@ data class WireTransaction(val inputStates: List,
val commands: List) : SerializeableWithKryo {
fun serializeForSignature(): ByteArray = serialize()
- fun toLedgerTransaction(timestamp: Instant, institutionKeyMap: Map, originalHash: SecureHash): LedgerTransaction {
+ fun toLedgerTransaction(timestamp: Instant?, institutionKeyMap: Map, originalHash: SecureHash): LedgerTransaction {
val authenticatedArgs = commands.map {
val institutions = it.pubkeys.mapNotNull { pk -> institutionKeyMap[pk] }
AuthenticatedObject(it.pubkeys, institutions, it.command)
@@ -133,6 +133,7 @@ class PartialTransaction(private val inputStates: MutableList
*/
interface TimestamperService {
fun timestamp(hash: SecureHash): ByteArray
+ fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant
}
data class SignedWireTransaction(val txBits: ByteArray, val sigs: List) : SerializeableWithKryo {
@@ -170,10 +171,14 @@ data class SignedWireTransaction(val txBits: ByteArray, val sigs: List): LedgerTransaction {
+ val stx: SignedWireTransaction = signedWireTX.deserialize()
+ val wtx: WireTransaction = stx.verify()
+ val instant: Instant? = if (timestamp.size != 0) timestamper.verifyTimestamp(signedWireTX.sha256(), timestamp) else null
+ return wtx.toLedgerTransaction(instant, institutionKeyMap, transactionID)
+ }
}
/**
@@ -202,8 +214,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 */
- val time: Instant,
+ /** 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?
@@ -223,7 +235,7 @@ data class LedgerTransaction(
data class TransactionForVerification(val inStates: List,
val outStates: List,
val commands: List>,
- val time: Instant,
+ val time: Instant?,
val origHash: SecureHash) {
override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
diff --git a/src/core/Utils.kt b/src/core/Utils.kt
index 109570382d..faca5cba59 100644
--- a/src/core/Utils.kt
+++ b/src/core/Utils.kt
@@ -26,4 +26,4 @@ open class OpaqueBytes(val bits: ByteArray) : SerializeableWithKryo {
val Int.days: Duration get() = Duration.ofDays(this.toLong())
val Int.hours: Duration get() = Duration.ofHours(this.toLong())
val Int.minutes: Duration get() = Duration.ofMinutes(this.toLong())
-val Int.seconds: Duration get() = Duration.ofSeconds(this.toLong())
\ No newline at end of file
+val Int.seconds: Duration get() = Duration.ofSeconds(this.toLong())
diff --git a/src/core/serialization/Kryo.kt b/src/core/serialization/Kryo.kt
index 04d184d38c..b16a5b9817 100644
--- a/src/core/serialization/Kryo.kt
+++ b/src/core/serialization/Kryo.kt
@@ -12,6 +12,7 @@ import core.*
import java.io.ByteArrayOutputStream
import java.lang.reflect.InvocationTargetException
import java.security.KeyPairGenerator
+import java.security.PublicKey
import java.time.Instant
import java.util.*
import kotlin.reflect.KClass
@@ -209,6 +210,7 @@ fun createKryo(): Kryo {
register(Currency::class.java, JavaSerializer()) // Only serialises the currency code as a string.
register(UNUSED_EC_KEYPAIR.private.javaClass, JavaSerializer())
register(UNUSED_EC_KEYPAIR.public.javaClass, JavaSerializer())
+ register(PublicKey::class.java, JavaSerializer())
// Now register platform types.
registerDataClass()
@@ -220,16 +222,36 @@ fun createKryo(): Kryo {
registerDataClass()
registerDataClass()
registerDataClass()
+ registerDataClass()
+
+ // Can't use data classes for this in Kotlin 1.0 due to lack of support for inheritance: must write a manual
+ // serialiser instead :(
+ register(DigitalSignature.WithKey::class.java, object : Serializer(false, true) {
+ override fun write(kryo: Kryo, output: Output, sig: DigitalSignature.WithKey) {
+ output.writeVarInt(sig.bits.size, true)
+ output.write(sig.bits)
+ output.writeInt(sig.covering, true)
+ kryo.writeObject(output, sig.by)
+ }
+
+ override fun read(kryo: Kryo, input: Input, type: Class): DigitalSignature.WithKey {
+ val sigLen = input.readVarInt(true)
+ val sigBits = input.readBytes(sigLen)
+ val covering = input.readInt(true)
+ val pubkey = kryo.readObject(input, PublicKey::class.java)
+ return DigitalSignature.WithKey(pubkey, sigBits, covering)
+ }
+ })
// TODO: This is obviously a short term hack: there needs to be a way to bundle up and register contracts.
registerDataClass()
- register(Cash.Commands.Move.javaClass)
+ register(Cash.Commands.Move::class.java)
registerDataClass()
registerDataClass()
registerDataClass()
- register(CommercialPaper.Commands.Move.javaClass)
- register(CommercialPaper.Commands.Redeem.javaClass)
- register(CommercialPaper.Commands.Issue.javaClass)
+ register(CommercialPaper.Commands.Move::class.java)
+ register(CommercialPaper.Commands.Redeem::class.java)
+ register(CommercialPaper.Commands.Issue::class.java)
// And for unit testing ...
registerDataClass()
diff --git a/tests/contracts/CashTests.kt b/tests/contracts/CashTests.kt
index e5130ea3fa..50f188e011 100644
--- a/tests/contracts/CashTests.kt
+++ b/tests/contracts/CashTests.kt
@@ -38,19 +38,19 @@ class CashTests {
}
tweak {
output { outState }
- arg(DUMMY_PUBKEY_2) { Cash.Commands.Move }
+ arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
this `fails requirement` "the owning keys are the same as the signing keys"
}
tweak {
output { outState }
output { outState.editInstitution(MINI_CORP) }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
+ arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "at least one cash input"
}
// Simple reallocation works.
tweak {
output { outState }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
+ arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.accepts()
}
}
@@ -62,7 +62,7 @@ class CashTests {
transaction {
input { DummyContract.State() }
output { outState }
- arg { Cash.Commands.Move }
+ arg { Cash.Commands.Move() }
this `fails requirement` "there is at least one cash input"
}
@@ -105,7 +105,7 @@ class CashTests {
fun testMergeSplit() {
// Splitting value works.
transaction {
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
+ arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
tweak {
input { inState }
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
@@ -176,7 +176,7 @@ class CashTests {
input { inState }
input { inState.editInstitution(MINI_CORP) }
output { outState }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
+ arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "at issuer MiniCorp the amounts balance"
}
// Can't combine two different deposits at the same issuer.
@@ -197,7 +197,7 @@ class CashTests {
tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS) }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
+ arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "the amounts balance"
}
@@ -206,7 +206,7 @@ class CashTests {
this `fails requirement` "required contracts.Cash.Commands.Move command"
tweak {
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
+ arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.accepts()
}
}
@@ -219,7 +219,7 @@ class CashTests {
output { inState.copy(amount = inState.amount - 200.DOLLARS).editInstitution(MINI_CORP) }
output { inState.copy(amount = inState.amount - 200.DOLLARS) }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
+ arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "at issuer MegaCorp the amounts balance"
@@ -253,7 +253,7 @@ class CashTests {
// This works.
output { inState.copy(owner = DUMMY_PUBKEY_2) }
output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
+ arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.accepts()
}
@@ -272,7 +272,7 @@ class CashTests {
input { pounds }
output { inState `owned by` DUMMY_PUBKEY_2 }
output { pounds `owned by` DUMMY_PUBKEY_1 }
- arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Cash.Commands.Move }
+ arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Cash.Commands.Move() }
this.accepts()
}
diff --git a/tests/contracts/CommercialPaperTests.kt b/tests/contracts/CommercialPaperTests.kt
index 7cdd68f4ef..f69daa1ca7 100644
--- a/tests/contracts/CommercialPaperTests.kt
+++ b/tests/contracts/CommercialPaperTests.kt
@@ -30,7 +30,7 @@ class CommercialPaperTests {
transactionGroup {
transaction {
output { PAPER_1 }
- arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue }
+ arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
}
expectFailureOfTx(1, "signed by the claimed issuer")
@@ -42,7 +42,7 @@ class CommercialPaperTests {
transactionGroup {
transaction {
output { PAPER_1.copy(faceValue = 0.DOLLARS) }
- arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue }
+ arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
}
expectFailureOfTx(1, "face value is not zero")
@@ -54,7 +54,7 @@ class CommercialPaperTests {
transactionGroup {
transaction {
output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) }
- arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue }
+ arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
}
expectFailureOfTx(1, "maturity date is not in the past")
@@ -70,7 +70,7 @@ class CommercialPaperTests {
transaction {
input("paper")
output { PAPER_1 }
- arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue }
+ arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
}
expectFailureOfTx(1, "there is no input state")
@@ -101,7 +101,7 @@ class CommercialPaperTests {
// Some CP is issued onto the ledger by MegaCorp.
transaction {
output("paper") { PAPER_1 }
- arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue }
+ arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
}
// The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
@@ -111,8 +111,8 @@ class CommercialPaperTests {
input("alice's $900")
output { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
output("alice's paper") { "paper".output `owned by` ALICE }
- arg(ALICE) { Cash.Commands.Move }
- arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move }
+ arg(ALICE) { Cash.Commands.Move() }
+ arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
}
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
@@ -126,8 +126,8 @@ class CommercialPaperTests {
if (!destroyPaperAtRedemption)
output { "paper".output }
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move }
- arg(ALICE) { CommercialPaper.Commands.Redeem }
+ arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
+ arg(ALICE) { CommercialPaper.Commands.Redeem() }
}
}
}
diff --git a/tests/core/TransactionGroupTests.kt b/tests/core/TransactionGroupTests.kt
index dc5a566643..cc8ff26a61 100644
--- a/tests/core/TransactionGroupTests.kt
+++ b/tests/core/TransactionGroupTests.kt
@@ -20,12 +20,12 @@ class TransactionGroupTests {
transaction {
input("£1000")
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE }
- arg(MINI_CORP_PUBKEY) { Cash.Commands.Move }
+ arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
}
transaction {
input("alice's £1000")
- arg(ALICE) { Cash.Commands.Move }
+ arg(ALICE) { Cash.Commands.Move() }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) }
}
@@ -46,7 +46,7 @@ class TransactionGroupTests {
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB
output { HALF }
output { HALF }
- arg(MINI_CORP_PUBKEY) { Cash.Commands.Move }
+ arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
}
verify()
@@ -57,7 +57,7 @@ class TransactionGroupTests {
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE
output { HALF }
output { HALF }
- arg(MINI_CORP_PUBKEY) { Cash.Commands.Move }
+ arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
}
assertNotEquals(conflict1, conflict2)
@@ -89,7 +89,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())), TEST_TX_TIME, SecureHash.randomSHA256())
)
val e = assertFailsWith(TransactionResolutionException::class) {
@@ -110,7 +110,7 @@ class TransactionGroupTests {
input("£1000")
input("£1000")
output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) }
- arg(MINI_CORP_PUBKEY) { Cash.Commands.Move }
+ arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
}
assertFailsWith(TransactionConflictException::class) {
diff --git a/tests/core/serialization/TransactionSerializationTests.kt b/tests/core/serialization/TransactionSerializationTests.kt
index 1b592fb5f7..fedd90a982 100644
--- a/tests/core/serialization/TransactionSerializationTests.kt
+++ b/tests/core/serialization/TransactionSerializationTests.kt
@@ -2,13 +2,13 @@ package core.serialization
import contracts.Cash
import core.*
-import core.testutils.DUMMY_PUBKEY_1
-import core.testutils.MINI_CORP
-import core.testutils.TestUtils
+import core.testutils.*
import org.junit.Before
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).
@@ -23,7 +23,7 @@ class TransactionSerializationTests {
@Before
fun setup() {
tx = PartialTransaction(
- fakeStateRef, outputState, changeState, WireCommand(Cash.Commands.Move, arrayListOf(TestUtils.keypair.public))
+ fakeStateRef, outputState, changeState, WireCommand(Cash.Commands.Move(), arrayListOf(TestUtils.keypair.public))
)
}
@@ -69,10 +69,27 @@ class TransactionSerializationTests {
// If the signature was replaced in transit, we don't like it.
assertFailsWith(SignatureException::class) {
val tx2 = PartialTransaction(fakeStateRef, outputState, changeState,
- WireCommand(Cash.Commands.Move, arrayListOf(TestUtils.keypair2.public)))
+ WireCommand(Cash.Commands.Move(), arrayListOf(TestUtils.keypair2.public)))
tx2.signWith(TestUtils.keypair2)
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
}
}
+
+ @Test
+ fun timestamp() {
+ tx.signWith(TestUtils.keypair)
+ val ttx = tx.toSignedTransaction().toTimestampedTransactionWithoutTime()
+ val ltx = ttx.verifyToLedgerTransaction(DUMMY_TIMESTAMPER, TEST_KEYS_TO_CORP_MAP)
+ assertEquals(tx.commands().map { it.command }, 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, TEST_KEYS_TO_CORP_MAP)
+ assertEquals(TEST_TX_TIME, ltx2.time)
+ }
}
\ No newline at end of file
diff --git a/tests/core/testutils/TestUtils.kt b/tests/core/testutils/TestUtils.kt
index faed264157..72a91001ff 100644
--- a/tests/core/testutils/TestUtils.kt
+++ b/tests/core/testutils/TestUtils.kt
@@ -2,8 +2,13 @@
package core.testutils
+import com.google.common.io.BaseEncoding
import contracts.*
import core.*
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.DataInputStream
+import java.io.DataOutputStream
import java.security.KeyPairGenerator
import java.security.PublicKey
import java.time.Instant
@@ -47,6 +52,33 @@ val TEST_PROGRAM_MAP: Map = mapOf(
DUMMY_PROGRAM_ID to DummyContract
)
+/**
+ * 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.
+ */
+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()
+ }
+
+ 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)
+ }
+}
+
+val DUMMY_TIMESTAMPER = DummyTimestamper()
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.