mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
Flesh out timestamping logic and fix various serialisation related bugs that it exposes. Timestamps are optional, so update the CommercialPaper contract and tutorial to reflect that.
This commit is contained in:
parent
dacfe299f8
commit
42eed3e0a3
20
docs/build/html/_sources/tutorial.txt
vendored
20
docs/build/html/_sources/tutorial.txt
vendored
@ -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<State> group : groups) {
|
||||
List<State> inputs = group.getInputs();
|
||||
List<State> 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
|
||||
|
2
docs/build/html/searchindex.js
vendored
2
docs/build/html/searchindex.js
vendored
File diff suppressed because one or more lines are too long
25
docs/build/html/tutorial.html
vendored
25
docs/build/html/tutorial.html
vendored
@ -418,7 +418,8 @@ trade many different pieces of commercial paper in a single atomic step.</p>
|
||||
<p>After extracting the command and the groups, we then iterate over each group and verify it meets the required business
|
||||
logic.</p>
|
||||
<div class="codeset container">
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre><span class="k">for</span> <span class="p">(</span><span class="n">group</span> <span class="k">in</span> <span class="n">groups</span><span class="p">)</span> <span class="p">{</span>
|
||||
<div class="highlight-kotlin"><div class="highlight"><pre><span class="k">val</span> <span class="py">time</span> <span class="p">=</span> <span class="n">tx</span><span class="p">.</span><span class="n">time</span>
|
||||
<span class="k">for</span> <span class="p">(</span><span class="n">group</span> <span class="k">in</span> <span class="n">groups</span><span class="p">)</span> <span class="p">{</span>
|
||||
<span class="k">when</span> <span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">value</span><span class="p">)</span> <span class="p">{</span>
|
||||
<span class="k">is</span> <span class="n">Commands</span><span class="p">.</span><span class="n">Move</span> <span class="p">-></span> <span class="p">{</span>
|
||||
<span class="k">val</span> <span class="py">input</span> <span class="p">=</span> <span class="n">group</span><span class="p">.</span><span class="n">inputs</span><span class="p">.</span><span class="n">single</span><span class="p">()</span>
|
||||
@ -431,8 +432,9 @@ logic.</p>
|
||||
<span class="k">is</span> <span class="n">Commands</span><span class="p">.</span><span class="n">Redeem</span> <span class="p">-></span> <span class="p">{</span>
|
||||
<span class="k">val</span> <span class="py">input</span> <span class="p">=</span> <span class="n">group</span><span class="p">.</span><span class="n">inputs</span><span class="p">.</span><span class="n">single</span><span class="p">()</span>
|
||||
<span class="k">val</span> <span class="py">received</span> <span class="p">=</span> <span class="n">tx</span><span class="p">.</span><span class="n">outStates</span><span class="p">.</span><span class="n">sumCashBy</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">owner</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="p">(</span><span class="n">time</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="k">throw</span> <span class="n">IllegalArgumentException</span><span class="p">(</span><span class="s">"Redemption transactions must be timestamped"</span><span class="p">)</span>
|
||||
<span class="n">requireThat</span> <span class="p">{</span>
|
||||
<span class="s">"the paper must have matured"</span> <span class="k">by</span> <span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">maturityDate</span> <span class="p"><</span> <span class="n">tx</span><span class="p">.</span><span class="n">time</span><span class="p">)</span>
|
||||
<span class="s">"the paper must have matured"</span> <span class="k">by</span> <span class="p">(</span><span class="n">time</span> <span class="p">></span> <span class="n">input</span><span class="p">.</span><span class="n">maturityDate</span><span class="p">)</span>
|
||||
<span class="s">"the received amount equals the face value"</span> <span class="k">by</span> <span class="p">(</span><span class="n">received</span> <span class="p">==</span> <span class="n">input</span><span class="p">.</span><span class="n">faceValue</span><span class="p">)</span>
|
||||
<span class="s">"the paper must be destroyed"</span> <span class="k">by</span> <span class="n">group</span><span class="p">.</span><span class="n">outputs</span><span class="p">.</span><span class="n">isEmpty</span><span class="p">()</span>
|
||||
<span class="s">"the transaction is signed by the owner of the CP"</span> <span class="k">by</span> <span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">signers</span><span class="p">.</span><span class="n">contains</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">owner</span><span class="p">))</span>
|
||||
@ -441,12 +443,13 @@ logic.</p>
|
||||
|
||||
<span class="k">is</span> <span class="n">Commands</span><span class="p">.</span><span class="n">Issue</span> <span class="p">-></span> <span class="p">{</span>
|
||||
<span class="k">val</span> <span class="py">output</span> <span class="p">=</span> <span class="n">group</span><span class="p">.</span><span class="n">outputs</span><span class="p">.</span><span class="n">single</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="p">(</span><span class="n">time</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="k">throw</span> <span class="n">IllegalArgumentException</span><span class="p">(</span><span class="s">"Issuance transactions must be timestamped"</span><span class="p">)</span>
|
||||
<span class="n">requireThat</span> <span class="p">{</span>
|
||||
<span class="c1">// Don't allow people to issue commercial paper under other entities identities.</span>
|
||||
<span class="s">"the issuance is signed by the claimed issuer of the paper"</span> <span class="k">by</span>
|
||||
<span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">signers</span><span class="p">.</span><span class="n">contains</span><span class="p">(</span><span class="n">output</span><span class="p">.</span><span class="n">issuance</span><span class="p">.</span><span class="n">institution</span><span class="p">.</span><span class="n">owningKey</span><span class="p">))</span>
|
||||
<span class="s">"the face value is not zero"</span> <span class="k">by</span> <span class="p">(</span><span class="n">output</span><span class="p">.</span><span class="n">faceValue</span><span class="p">.</span><span class="n">pennies</span> <span class="p">></span> <span class="m">0</span><span class="p">)</span>
|
||||
<span class="s">"the maturity date is not in the past"</span> <span class="k">by</span> <span class="p">(</span><span class="n">output</span><span class="p">.</span><span class="n">maturityDate</span> <span class="p">></span> <span class="n">tx</span><span class="p">.</span><span class="n">time</span><span class="p">)</span>
|
||||
<span class="s">"the maturity date is not in the past"</span> <span class="k">by</span> <span class="p">(</span><span class="n">time</span> <span class="p"><</span> <span class="n">output</span><span class="p">.</span><span class="n">maturityDate</span> <span class="p">)</span>
|
||||
<span class="c1">// Don't allow an existing CP state to be replaced by this issuance.</span>
|
||||
<span class="s">"there is no input state"</span> <span class="k">by</span> <span class="n">group</span><span class="p">.</span><span class="n">inputs</span><span class="p">.</span><span class="n">isEmpty</span><span class="p">()</span>
|
||||
<span class="p">}</span>
|
||||
@ -458,7 +461,8 @@ logic.</p>
|
||||
<span class="p">}</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="highlight-java"><div class="highlight"><pre><span class="k">for</span> <span class="o">(</span><span class="n">InOutGroup</span><span class="o"><</span><span class="n">State</span><span class="o">></span> <span class="n">group</span> <span class="o">:</span> <span class="n">groups</span><span class="o">)</span> <span class="o">{</span>
|
||||
<div class="highlight-java"><div class="highlight"><pre><span class="n">Instant</span> <span class="n">time</span> <span class="o">=</span> <span class="n">tx</span><span class="o">.</span><span class="na">getTime</span><span class="o">();</span> <span class="c1">// Can be null/missing.</span>
|
||||
<span class="k">for</span> <span class="o">(</span><span class="n">InOutGroup</span><span class="o"><</span><span class="n">State</span><span class="o">></span> <span class="n">group</span> <span class="o">:</span> <span class="n">groups</span><span class="o">)</span> <span class="o">{</span>
|
||||
<span class="n">List</span><span class="o"><</span><span class="n">State</span><span class="o">></span> <span class="n">inputs</span> <span class="o">=</span> <span class="n">group</span><span class="o">.</span><span class="na">getInputs</span><span class="o">();</span>
|
||||
<span class="n">List</span><span class="o"><</span><span class="n">State</span><span class="o">></span> <span class="n">outputs</span> <span class="o">=</span> <span class="n">group</span><span class="o">.</span><span class="na">getOutputs</span><span class="o">();</span>
|
||||
|
||||
@ -478,9 +482,11 @@ logic.</p>
|
||||
<span class="k">throw</span> <span class="k">new</span> <span class="n">IllegalStateException</span><span class="o">(</span><span class="s">"Failed requirement: the output state is the same as the input state except for owner"</span><span class="o">);</span>
|
||||
<span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">cmd</span><span class="o">.</span><span class="na">getValue</span><span class="o">()</span> <span class="k">instanceof</span> <span class="n">JavaCommercialPaper</span><span class="o">.</span><span class="na">Commands</span><span class="o">.</span><span class="na">Redeem</span><span class="o">)</span> <span class="o">{</span>
|
||||
<span class="n">Amount</span> <span class="n">received</span> <span class="o">=</span> <span class="n">CashKt</span><span class="o">.</span><span class="na">sumCashOrNull</span><span class="o">(</span><span class="n">inputs</span><span class="o">);</span>
|
||||
<span class="k">if</span> <span class="o">(</span><span class="n">time</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span>
|
||||
<span class="k">throw</span> <span class="k">new</span> <span class="n">IllegalArgumentException</span><span class="o">(</span><span class="s">"Redemption transactions must be timestamped"</span><span class="o">);</span>
|
||||
<span class="k">if</span> <span class="o">(</span><span class="n">received</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span>
|
||||
<span class="k">throw</span> <span class="k">new</span> <span class="n">IllegalStateException</span><span class="o">(</span><span class="s">"Failed requirement: no cash being redeemed"</span><span class="o">);</span>
|
||||
<span class="k">if</span> <span class="o">(</span><span class="n">input</span><span class="o">.</span><span class="na">getMaturityDate</span><span class="o">().</span><span class="na">isAfter</span><span class="o">(</span><span class="n">tx</span><span class="o">.</span><span class="na">getTime</span><span class="o">()))</span>
|
||||
<span class="k">if</span> <span class="o">(</span><span class="n">input</span><span class="o">.</span><span class="na">getMaturityDate</span><span class="o">().</span><span class="na">isAfter</span><span class="o">(</span><span class="n">time</span><span class="o">))</span>
|
||||
<span class="k">throw</span> <span class="k">new</span> <span class="n">IllegalStateException</span><span class="o">(</span><span class="s">"Failed requirement: the paper must have matured"</span><span class="o">);</span>
|
||||
<span class="k">if</span> <span class="o">(!</span><span class="n">input</span><span class="o">.</span><span class="na">getFaceValue</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="n">received</span><span class="o">))</span>
|
||||
<span class="k">throw</span> <span class="k">new</span> <span class="n">IllegalStateException</span><span class="o">(</span><span class="s">"Failed requirement: the received amount equals the face value"</span><span class="o">);</span>
|
||||
@ -494,6 +500,15 @@ logic.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>This loop is the core logic of the contract.</p>
|
||||
<p>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.</p>
|
||||
<div class="admonition note">
|
||||
<p class="first admonition-title">Note</p>
|
||||
<p class="last">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. <cite>someDate > time</cite>, it has to be <cite>time < someDate</cite>. So it’s good practice to
|
||||
always write the transaction timestamp first.</p>
|
||||
</div>
|
||||
<p>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
|
||||
<code class="docutils literal"><span class="pre">single()</span></code> method is a static <em>extension method</em> defined by the Kotlin standard library: given a list, it throws an
|
||||
|
@ -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<State> group : groups) {
|
||||
List<State> inputs = group.getInputs();
|
||||
List<State> 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
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<CommercialPaper.Commands>()
|
||||
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<State>, 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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Command> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
|
||||
|
||||
Instant time = tx.getTime(); // Can be null/missing.
|
||||
|
||||
for (InOutGroup<State> group : groups) {
|
||||
List<State> inputs = group.getInputs();
|
||||
List<State> 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");
|
||||
|
@ -63,6 +63,8 @@ class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey>, 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]"
|
||||
}
|
||||
|
||||
|
@ -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<out T : Any>(
|
||||
val signers: List<PublicKey>,
|
||||
|
@ -51,7 +51,7 @@ data class WireTransaction(val inputStates: List<ContractStateRef>,
|
||||
val commands: List<WireCommand>) : SerializeableWithKryo {
|
||||
fun serializeForSignature(): ByteArray = serialize()
|
||||
|
||||
fun toLedgerTransaction(timestamp: Instant, institutionKeyMap: Map<PublicKey, Institution>, originalHash: SecureHash): LedgerTransaction {
|
||||
fun toLedgerTransaction(timestamp: Instant?, institutionKeyMap: Map<PublicKey, Institution>, 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<ContractStateRef>
|
||||
*/
|
||||
interface TimestamperService {
|
||||
fun timestamp(hash: SecureHash): ByteArray
|
||||
fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant
|
||||
}
|
||||
|
||||
data class SignedWireTransaction(val txBits: ByteArray, val sigs: List<DigitalSignature.WithKey>) : SerializeableWithKryo {
|
||||
@ -170,10 +171,14 @@ data class SignedWireTransaction(val txBits: ByteArray, val sigs: List<DigitalSi
|
||||
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()))
|
||||
}
|
||||
|
||||
/** Returns a [TimestampedWireTransaction] with an empty byte array as the timestamp: this means, no time was provided. */
|
||||
fun toTimestampedTransactionWithoutTime() = TimestampedWireTransaction(serialize(), ByteArray(0))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -182,12 +187,19 @@ data class SignedWireTransaction(val txBits: ByteArray, val sigs: List<DigitalSi
|
||||
*/
|
||||
data class TimestampedWireTransaction(
|
||||
/** A serialised SignedWireTransaction */
|
||||
val wireTX: ByteArray,
|
||||
val signedWireTX: ByteArray,
|
||||
|
||||
/** Signature from a timestamping authority. For instance using RFC 3161 */
|
||||
val timestamp: ByteArray
|
||||
) : SerializeableWithKryo {
|
||||
val transactionID: SecureHash = serialize().sha256()
|
||||
|
||||
fun verifyToLedgerTransaction(timestamper: TimestamperService, institutionKeyMap: Map<PublicKey, Institution>): 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<ContractState>,
|
||||
/** Arbitrary data passed to the program of each input state. */
|
||||
val commands: List<AuthenticatedObject<Command>>,
|
||||
/** 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<ContractState>,
|
||||
val outStates: List<ContractState>,
|
||||
val commands: List<AuthenticatedObject<Command>>,
|
||||
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
|
||||
|
@ -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())
|
||||
val Int.seconds: Duration get() = Duration.ofSeconds(this.toLong())
|
||||
|
@ -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<SecureHash.SHA256>()
|
||||
@ -220,16 +222,36 @@ fun createKryo(): Kryo {
|
||||
registerDataClass<ContractStateRef>()
|
||||
registerDataClass<WireTransaction>()
|
||||
registerDataClass<WireCommand>()
|
||||
registerDataClass<TimestampedWireTransaction>()
|
||||
|
||||
// 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<DigitalSignature.WithKey>(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>): 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<Cash.State>()
|
||||
register(Cash.Commands.Move.javaClass)
|
||||
register(Cash.Commands.Move::class.java)
|
||||
registerDataClass<Cash.Commands.Exit>()
|
||||
registerDataClass<Cash.Commands.Issue>()
|
||||
registerDataClass<CommercialPaper.State>()
|
||||
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<DummyPublicKey>()
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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<SecureHash, Contract> = 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.
|
||||
|
Loading…
Reference in New Issue
Block a user