Merge branch 'finish-timestamping'

# Conflicts:
#	docs/build/html/searchindex.js
#	src/core/serialization/Kryo.kt
This commit is contained in:
Mike Hearn 2015-11-30 16:30:21 +00:00
commit b8a50a65cf
19 changed files with 214 additions and 70 deletions

View File

@ -320,6 +320,7 @@ logic.
.. sourcecode:: kotlin .. sourcecode:: kotlin
val time = tx.time
for (group in groups) { for (group in groups) {
when (command.value) { when (command.value) {
is Commands.Move -> { is Commands.Move -> {
@ -333,8 +334,9 @@ logic.
is Commands.Redeem -> { is Commands.Redeem -> {
val input = group.inputs.single() val input = group.inputs.single()
val received = tx.outStates.sumCashBy(input.owner) val received = tx.outStates.sumCashBy(input.owner)
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
requireThat { 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 received amount equals the face value" by (received == input.faceValue)
"the paper must be destroyed" by group.outputs.isEmpty() "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)) "the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
@ -343,12 +345,13 @@ logic.
is Commands.Issue -> { is Commands.Issue -> {
val output = group.outputs.single() val output = group.outputs.single()
if (time == null) throw IllegalArgumentException("Issuance transactions must be timestamped")
requireThat { requireThat {
// Don't allow people to issue commercial paper under other entities identities. // Don't allow people to issue commercial paper under other entities identities.
"the issuance is signed by the claimed issuer of the paper" by "the issuance is signed by the claimed issuer of the paper" by
(command.signers.contains(output.issuance.institution.owningKey)) (command.signers.contains(output.issuance.institution.owningKey))
"the face value is not zero" by (output.faceValue.pennies > 0) "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. // Don't allow an existing CP state to be replaced by this issuance.
"there is no input state" by group.inputs.isEmpty() "there is no input state" by group.inputs.isEmpty()
} }
@ -361,6 +364,7 @@ logic.
.. sourcecode:: java .. sourcecode:: java
Instant time = tx.getTime(); // Can be null/missing.
for (InOutGroup<State> group : groups) { for (InOutGroup<State> group : groups) {
List<State> inputs = group.getInputs(); List<State> inputs = group.getInputs();
List<State> outputs = group.getOutputs(); 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"); 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) { } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) {
Amount received = CashKt.sumCashOrNull(inputs); Amount received = CashKt.sumCashOrNull(inputs);
if (time == null)
throw new IllegalArgumentException("Redemption transactions must be timestamped");
if (received == null) if (received == null)
throw new IllegalStateException("Failed requirement: no cash being redeemed"); 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"); throw new IllegalStateException("Failed requirement: the paper must have matured");
if (!input.getFaceValue().equals(received)) if (!input.getFaceValue().equals(received))
throw new IllegalStateException("Failed requirement: the received amount equals the face value"); 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. 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 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 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 ``single()`` method is a static *extension method* defined by the Kotlin standard library: given a list, it throws an

File diff suppressed because one or more lines are too long

View File

@ -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 <p>After extracting the command and the groups, we then iterate over each group and verify it meets the required business
logic.</p> logic.</p>
<div class="codeset container"> <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">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">-&gt;</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">-&gt;</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">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">-&gt;</span> <span class="p">{</span> <span class="k">is</span> <span class="n">Commands</span><span class="p">.</span><span class="n">Redeem</span> <span class="p">-&gt;</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">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">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">&quot;Redemption transactions must be timestamped&quot;</span><span class="p">)</span>
<span class="n">requireThat</span> <span class="p">{</span> <span class="n">requireThat</span> <span class="p">{</span>
<span class="s">&quot;the paper must have matured&quot;</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">&lt;</span> <span class="n">tx</span><span class="p">.</span><span class="n">time</span><span class="p">)</span> <span class="s">&quot;the paper must have matured&quot;</span> <span class="k">by</span> <span class="p">(</span><span class="n">time</span> <span class="p">&gt;</span> <span class="n">input</span><span class="p">.</span><span class="n">maturityDate</span><span class="p">)</span>
<span class="s">&quot;the received amount equals the face value&quot;</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">&quot;the received amount equals the face value&quot;</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">&quot;the paper must be destroyed&quot;</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">&quot;the paper must be destroyed&quot;</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">&quot;the transaction is signed by the owner of the CP&quot;</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> <span class="s">&quot;the transaction is signed by the owner of the CP&quot;</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">-&gt;</span> <span class="p">{</span> <span class="k">is</span> <span class="n">Commands</span><span class="p">.</span><span class="n">Issue</span> <span class="p">-&gt;</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">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">&quot;Issuance transactions must be timestamped&quot;</span><span class="p">)</span>
<span class="n">requireThat</span> <span class="p">{</span> <span class="n">requireThat</span> <span class="p">{</span>
<span class="c1">// Don&#39;t allow people to issue commercial paper under other entities identities.</span> <span class="c1">// Don&#39;t allow people to issue commercial paper under other entities identities.</span>
<span class="s">&quot;the issuance is signed by the claimed issuer of the paper&quot;</span> <span class="k">by</span> <span class="s">&quot;the issuance is signed by the claimed issuer of the paper&quot;</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="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">&quot;the face value is not zero&quot;</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">&gt;</span> <span class="m">0</span><span class="p">)</span> <span class="s">&quot;the face value is not zero&quot;</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">&gt;</span> <span class="m">0</span><span class="p">)</span>
<span class="s">&quot;the maturity date is not in the past&quot;</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">&gt;</span> <span class="n">tx</span><span class="p">.</span><span class="n">time</span><span class="p">)</span> <span class="s">&quot;the maturity date is not in the past&quot;</span> <span class="k">by</span> <span class="p">(</span><span class="n">time</span> <span class="p">&lt;</span> <span class="n">output</span><span class="p">.</span><span class="n">maturityDate</span> <span class="p">)</span>
<span class="c1">// Don&#39;t allow an existing CP state to be replaced by this issuance.</span> <span class="c1">// Don&#39;t allow an existing CP state to be replaced by this issuance.</span>
<span class="s">&quot;there is no input state&quot;</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="s">&quot;there is no input state&quot;</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> <span class="p">}</span>
@ -458,7 +461,8 @@ logic.</p>
<span class="p">}</span> <span class="p">}</span>
</pre></div> </pre></div>
</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">&lt;</span><span class="n">State</span><span class="o">&gt;</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">&lt;</span><span class="n">State</span><span class="o">&gt;</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">&lt;</span><span class="n">State</span><span class="o">&gt;</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">&lt;</span><span class="n">State</span><span class="o">&gt;</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">&lt;</span><span class="n">State</span><span class="o">&gt;</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> <span class="n">List</span><span class="o">&lt;</span><span class="n">State</span><span class="o">&gt;</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">&quot;Failed requirement: the output state is the same as the input state except for owner&quot;</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">&quot;Failed requirement: the output state is the same as the input state except for owner&quot;</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="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="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">&quot;Redemption transactions must be timestamped&quot;</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">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">&quot;Failed requirement: no cash being redeemed&quot;</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">&quot;Failed requirement: no cash being redeemed&quot;</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">&quot;Failed requirement: the paper must have matured&quot;</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">&quot;Failed requirement: the paper must have matured&quot;</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">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">&quot;Failed requirement: the received amount equals the face value&quot;</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">&quot;Failed requirement: the received amount equals the face value&quot;</span><span class="o">);</span>
@ -494,6 +500,15 @@ logic.</p>
</div> </div>
</div> </div>
<p>This loop is the core logic of the contract.</p> <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&#8217;t forget to check if it&#8217;s missing. Unfortunately due to the need for smooth Java interop, this
check won&#8217;t happen if we write e.g. <cite>someDate &gt; time</cite>, it has to be <cite>time &lt; someDate</cite>. So it&#8217;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 <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 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 <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

View File

@ -320,6 +320,7 @@ logic.
.. sourcecode:: kotlin .. sourcecode:: kotlin
val time = tx.time
for (group in groups) { for (group in groups) {
when (command.value) { when (command.value) {
is Commands.Move -> { is Commands.Move -> {
@ -333,8 +334,9 @@ logic.
is Commands.Redeem -> { is Commands.Redeem -> {
val input = group.inputs.single() val input = group.inputs.single()
val received = tx.outStates.sumCashBy(input.owner) val received = tx.outStates.sumCashBy(input.owner)
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
requireThat { 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 received amount equals the face value" by (received == input.faceValue)
"the paper must be destroyed" by group.outputs.isEmpty() "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)) "the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
@ -343,12 +345,13 @@ logic.
is Commands.Issue -> { is Commands.Issue -> {
val output = group.outputs.single() val output = group.outputs.single()
if (time == null) throw IllegalArgumentException("Issuance transactions must be timestamped")
requireThat { requireThat {
// Don't allow people to issue commercial paper under other entities identities. // Don't allow people to issue commercial paper under other entities identities.
"the issuance is signed by the claimed issuer of the paper" by "the issuance is signed by the claimed issuer of the paper" by
(command.signers.contains(output.issuance.institution.owningKey)) (command.signers.contains(output.issuance.institution.owningKey))
"the face value is not zero" by (output.faceValue.pennies > 0) "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. // Don't allow an existing CP state to be replaced by this issuance.
"there is no input state" by group.inputs.isEmpty() "there is no input state" by group.inputs.isEmpty()
} }
@ -361,6 +364,7 @@ logic.
.. sourcecode:: java .. sourcecode:: java
Instant time = tx.getTime(); // Can be null/missing.
for (InOutGroup<State> group : groups) { for (InOutGroup<State> group : groups) {
List<State> inputs = group.getInputs(); List<State> inputs = group.getInputs();
List<State> outputs = group.getOutputs(); 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"); 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) { } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) {
Amount received = CashKt.sumCashOrNull(inputs); Amount received = CashKt.sumCashOrNull(inputs);
if (time == null)
throw new IllegalArgumentException("Redemption transactions must be timestamped");
if (received == null) if (received == null)
throw new IllegalStateException("Failed requirement: no cash being redeemed"); 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"); throw new IllegalStateException("Failed requirement: the paper must have matured");
if (!input.getFaceValue().equals(received)) if (!input.getFaceValue().equals(received))
throw new IllegalStateException("Failed requirement: the received amount equals the face value"); 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. 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 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 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 ``single()`` method is a static *extension method* defined by the Kotlin standard library: given a list, it throws an

View File

@ -56,7 +56,7 @@ class Cash : Contract {
// Just for grouping // Just for grouping
interface Commands : Command { 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 * 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 gathered) tx.addInputState(state.ref)
for (state in outputs) tx.addOutputState(state) 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? // 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()))
} }
} }

View File

@ -40,11 +40,11 @@ class CommercialPaper : Contract {
} }
interface Commands : Command { interface Commands : Command {
object Move : Commands class Move : TypeOnlyCommand(), Commands
object Redeem : 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. // 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. // 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) { 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 // 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. // it for cash on or after the maturity date.
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>() val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
val time = tx.time
for (group in groups) { for (group in groups) {
when (command.value) { when (command.value) {
@ -68,8 +69,9 @@ class CommercialPaper : Contract {
is Commands.Redeem -> { is Commands.Redeem -> {
val input = group.inputs.single() val input = group.inputs.single()
val received = tx.outStates.sumCashBy(input.owner) val received = tx.outStates.sumCashBy(input.owner)
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
requireThat { 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 received amount equals the face value" by (received == input.faceValue)
"the paper must be destroyed" by group.outputs.isEmpty() "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)) "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 -> { is Commands.Issue -> {
val output = group.outputs.single() val output = group.outputs.single()
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
requireThat { requireThat {
// Don't allow people to issue commercial paper under other entities identities. // Don't allow people to issue commercial paper under other entities identities.
"the issuance is signed by the claimed issuer of the paper" by "the issuance is signed by the claimed issuer of the paper" by
(command.signers.contains(output.issuance.institution.owningKey)) (command.signers.contains(output.issuance.institution.owningKey))
"the face value is not zero" by (output.faceValue.pennies > 0) "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. // Don't allow an existing CP state to be replaced by this issuance.
"there is no input state" by group.inputs.isEmpty() "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 { fun craftIssue(issuance: InstitutionReference, faceValue: Amount, maturityDate: Instant): PartialTransaction {
val state = State(issuance, issuance.institution.owningKey, faceValue, maturityDate) 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) { fun craftMove(tx: PartialTransaction, paper: StateAndRef<State>, newOwner: PublicKey) {
tx.addInputState(paper.ref) tx.addInputState(paper.ref)
tx.addOutputState(paper.state.copy(owner = newOwner)) 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. // Add the cash movement using the states in our wallet.
Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet) Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet)
tx.addInputState(paper.ref) tx.addInputState(paper.ref)
tx.addArg(WireCommand(CommercialPaper.Commands.Redeem, paper.state.owner)) tx.addArg(WireCommand(CommercialPaper.Commands.Redeem(), paper.state.owner))
} }
} }

View File

@ -72,8 +72,9 @@ class CrowdFund : Contract {
val inputCash: List<Cash.State> = tx.inStates.filterIsInstance<Cash.State>() val inputCash: List<Cash.State> = tx.inStates.filterIsInstance<Cash.State>()
val pledge = outputCrowdFund.pledges.last() val pledge = outputCrowdFund.pledges.last()
val pledgedCash = outputCash.single() val pledgedCash = outputCash.single()
val time = tx.time ?: throw IllegalStateException("Transaction must be timestamped")
requireThat { requireThat {
"the funding is still open" by (inputCrowdFund.closingTime >= tx.time) "the funding is still open" by (time <= inputCrowdFund.closingTime)
// TODO "the transaction is signed by the owner of the pledge" by (command.signers.contains(inputCrowdFund.owner)) // TODO "the transaction is signed by the owner of the pledge" by (command.signers.contains(inputCrowdFund.owner))
"the transaction is signed by the pledge-maker" by (command.signers.contains(pledge.owner)) "the transaction is signed by the pledge-maker" by (command.signers.contains(pledge.owner))
"the pledge must be for a non-zero amount" by (pledge.amount.pennies > 0) "the pledge must be for a non-zero amount" by (pledge.amount.pennies > 0)
@ -102,8 +103,10 @@ class CrowdFund : Contract {
return true return true
} }
val time = tx.time ?: throw IllegalStateException("Transaction must be timestamped")
requireThat { requireThat {
"the closing date has past" by (tx.time >= outputCrowdFund.closingTime) "the closing date has past" by (time >= outputCrowdFund.closingTime)
"the pledges did not meet the target" by (inputCrowdFund.pledgeTotal < inputCrowdFund.fundingTarget) "the pledges did not meet the target" by (inputCrowdFund.pledgeTotal < inputCrowdFund.fundingTarget)
"the output cash returns equal the pledge total, if the target is not reached" by (outputCash.sumCash() == inputCrowdFund.pledgeTotal) "the output cash returns equal the pledge total, if the target is not reached" by (outputCash.sumCash() == inputCrowdFund.pledgeTotal)
"the output cash is distributed to the pledge-makers, if the target is not reached" by (checkReturns(inputCrowdFund, outputCash)) "the output cash is distributed to the pledge-makers, if the target is not reached" by (checkReturns(inputCrowdFund, outputCash))
@ -123,8 +126,9 @@ class CrowdFund : Contract {
is Commands.Funded -> { is Commands.Funded -> {
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single() val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
requireThat { val time = tx.time ?: throw IllegalStateException("Transaction must be timestamped")
"the closing date has past" by (tx.time >= outputCrowdFund.closingTime) requireThat {
"the closing date has past" by (time >= outputCrowdFund.closingTime)
"the input has an open state" by (!inputCrowdFund.closed) "the input has an open state" by (!inputCrowdFund.closed)
"the output registration has a closed state" by (outputCrowdFund.closed) "the output registration has a closed state" by (outputCrowdFund.closed)
// TODO how to simplify the boilerplate associated with unchanged elements // TODO how to simplify the boilerplate associated with unchanged elements

View File

@ -109,6 +109,8 @@ public class JavaCommercialPaper implements Contract {
// Find the command that instructs us what to do and check there's exactly one. // Find the command that instructs us what to do and check there's exactly one.
AuthenticatedObject<Command> cmd = requireSingleCommand(tx.getCommands(), Commands.class); AuthenticatedObject<Command> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
Instant time = tx.getTime(); // Can be null/missing.
for (InOutGroup<State> group : groups) { for (InOutGroup<State> group : groups) {
List<State> inputs = group.getInputs(); List<State> inputs = group.getInputs();
List<State> outputs = group.getOutputs(); 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"); 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) { } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) {
Amount received = CashKt.sumCashOrNull(inputs); Amount received = CashKt.sumCashOrNull(inputs);
if (time == null)
throw new IllegalArgumentException("Redemption transactions must be timestamped");
if (received == null) if (received == null)
throw new IllegalStateException("Failed requirement: no cash being redeemed"); 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"); throw new IllegalStateException("Failed requirement: the paper must have matured");
if (!input.getFaceValue().equals(received)) if (!input.getFaceValue().equals(received))
throw new IllegalStateException("Failed requirement: the received amount equals the face value"); throw new IllegalStateException("Failed requirement: the received amount equals the face value");

View File

@ -63,6 +63,8 @@ class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey>, Serializ
override fun getEncoded() = s.toByteArray() override fun getEncoded() = s.toByteArray()
override fun getFormat() = "ASN.1" override fun getFormat() = "ASN.1"
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded)) 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]" override fun toString() = "PUBKEY[$s]"
} }

View File

@ -49,6 +49,12 @@ data class InstitutionReference(val institution: Institution, val reference: Opa
/** Marker interface for classes that represent commands */ /** Marker interface for classes that represent commands */
interface Command : SerializeableWithKryo 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. */ /** 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>( data class AuthenticatedObject<out T : Any>(
val signers: List<PublicKey>, val signers: List<PublicKey>,

View File

@ -51,7 +51,7 @@ data class WireTransaction(val inputStates: List<ContractStateRef>,
val commands: List<WireCommand>) : SerializeableWithKryo { val commands: List<WireCommand>) : SerializeableWithKryo {
fun serializeForSignature(): ByteArray = serialize() 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 authenticatedArgs = commands.map {
val institutions = it.pubkeys.mapNotNull { pk -> institutionKeyMap[pk] } val institutions = it.pubkeys.mapNotNull { pk -> institutionKeyMap[pk] }
AuthenticatedObject(it.pubkeys, institutions, it.command) AuthenticatedObject(it.pubkeys, institutions, it.command)
@ -133,6 +133,7 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
*/ */
interface TimestamperService { interface TimestamperService {
fun timestamp(hash: SecureHash): ByteArray fun timestamp(hash: SecureHash): ByteArray
fun verifyTimestamp(hash: SecureHash, signedTimestamp: ByteArray): Instant
} }
data class SignedWireTransaction(val txBits: ByteArray, val sigs: List<DigitalSignature.WithKey>) : SerializeableWithKryo { 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 return wtx
} }
/** Uses the given timestamper service to calculate a signed timestamp and then returns a wrapper for both */
fun toTimestampedTransaction(timestamper: TimestamperService): TimestampedWireTransaction { fun toTimestampedTransaction(timestamper: TimestamperService): TimestampedWireTransaction {
val bits = serialize() val bits = serialize()
return TimestampedWireTransaction(bits, timestamper.timestamp(bits.sha256())) 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( data class TimestampedWireTransaction(
/** A serialised SignedWireTransaction */ /** A serialised SignedWireTransaction */
val wireTX: ByteArray, val signedWireTX: ByteArray,
/** Signature from a timestamping authority. For instance using RFC 3161 */ /** Signature from a timestamping authority. For instance using RFC 3161 */
val timestamp: ByteArray val timestamp: ByteArray
) : SerializeableWithKryo { ) : SerializeableWithKryo {
val transactionID: SecureHash = serialize().sha256() 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>, val outStates: List<ContractState>,
/** Arbitrary data passed to the program of each input state. */ /** Arbitrary data passed to the program of each input state. */
val commands: List<AuthenticatedObject<Command>>, val commands: List<AuthenticatedObject<Command>>,
/** The moment the transaction was timestamped for */ /** The moment the transaction was timestamped for, if a timestamp was present. */
val time: Instant, val time: Instant?,
/** The hash of the original serialised TimestampedWireTransaction or SignedTransaction */ /** The hash of the original serialised TimestampedWireTransaction or SignedTransaction */
val hash: SecureHash val hash: SecureHash
// TODO: nLockTime equivalent? // TODO: nLockTime equivalent?
@ -223,7 +235,7 @@ data class LedgerTransaction(
data class TransactionForVerification(val inStates: List<ContractState>, data class TransactionForVerification(val inStates: List<ContractState>,
val outStates: List<ContractState>, val outStates: List<ContractState>,
val commands: List<AuthenticatedObject<Command>>, val commands: List<AuthenticatedObject<Command>>,
val time: Instant, val time: Instant?,
val origHash: SecureHash) { val origHash: SecureHash) {
override fun hashCode() = origHash.hashCode() override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash

View File

@ -26,4 +26,4 @@ open class OpaqueBytes(val bits: ByteArray) : SerializeableWithKryo {
val Int.days: Duration get() = Duration.ofDays(this.toLong()) val Int.days: Duration get() = Duration.ofDays(this.toLong())
val Int.hours: Duration get() = Duration.ofHours(this.toLong()) val Int.hours: Duration get() = Duration.ofHours(this.toLong())
val Int.minutes: Duration get() = Duration.ofMinutes(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())

View File

@ -13,6 +13,7 @@ import core.*
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.PublicKey
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -210,6 +211,7 @@ fun createKryo(): Kryo {
register(Currency::class.java, JavaSerializer()) // Only serialises the currency code as a string. register(Currency::class.java, JavaSerializer()) // Only serialises the currency code as a string.
register(UNUSED_EC_KEYPAIR.private.javaClass, JavaSerializer()) register(UNUSED_EC_KEYPAIR.private.javaClass, JavaSerializer())
register(UNUSED_EC_KEYPAIR.public.javaClass, JavaSerializer()) register(UNUSED_EC_KEYPAIR.public.javaClass, JavaSerializer())
register(PublicKey::class.java, JavaSerializer())
// Now register platform types. // Now register platform types.
registerDataClass<SecureHash.SHA256>() registerDataClass<SecureHash.SHA256>()
@ -221,17 +223,36 @@ fun createKryo(): Kryo {
registerDataClass<ContractStateRef>() registerDataClass<ContractStateRef>()
registerDataClass<WireTransaction>() registerDataClass<WireTransaction>()
registerDataClass<WireCommand>() 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. // TODO: This is obviously a short term hack: there needs to be a way to bundle up and register contracts.
registerDataClass<Cash.State>() registerDataClass<Cash.State>()
register(Cash.Commands.Move.javaClass) register(Cash.Commands.Move::class.java)
registerDataClass<Cash.Commands.Exit>() registerDataClass<Cash.Commands.Exit>()
registerDataClass<Cash.Commands.Issue>() registerDataClass<Cash.Commands.Issue>()
registerDataClass<CommercialPaper.State>() registerDataClass<CommercialPaper.State>()
register(CommercialPaper.Commands.Move.javaClass) register(CommercialPaper.Commands.Move::class.java)
register(CommercialPaper.Commands.Redeem.javaClass) register(CommercialPaper.Commands.Redeem::class.java)
register(CommercialPaper.Commands.Issue.javaClass) register(CommercialPaper.Commands.Issue::class.java)
// Added for
registerDataClass<CrowdFund.State>() registerDataClass<CrowdFund.State>()
registerDataClass<CrowdFund.Pledge>() registerDataClass<CrowdFund.Pledge>()
register(CrowdFund.Commands.Register.javaClass) register(CrowdFund.Commands.Register.javaClass)

View File

@ -38,19 +38,19 @@ class CashTests {
} }
tweak { tweak {
output { outState } 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" this `fails requirement` "the owning keys are the same as the signing keys"
} }
tweak { tweak {
output { outState } output { outState }
output { outState.editInstitution(MINI_CORP) } 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" this `fails requirement` "at least one cash input"
} }
// Simple reallocation works. // Simple reallocation works.
tweak { tweak {
output { outState } output { outState }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.accepts() this.accepts()
} }
} }
@ -62,7 +62,7 @@ class CashTests {
transaction { transaction {
input { DummyContract.State() } input { DummyContract.State() }
output { outState } output { outState }
arg { Cash.Commands.Move } arg { Cash.Commands.Move() }
this `fails requirement` "there is at least one cash input" this `fails requirement` "there is at least one cash input"
} }
@ -105,7 +105,7 @@ class CashTests {
fun testMergeSplit() { fun testMergeSplit() {
// Splitting value works. // Splitting value works.
transaction { transaction {
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
tweak { tweak {
input { inState } input { inState }
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) } for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
@ -176,7 +176,7 @@ class CashTests {
input { inState } input { inState }
input { inState.editInstitution(MINI_CORP) } input { inState.editInstitution(MINI_CORP) }
output { outState } 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" this `fails requirement` "at issuer MiniCorp the amounts balance"
} }
// Can't combine two different deposits at the same issuer. // Can't combine two different deposits at the same issuer.
@ -197,7 +197,7 @@ class CashTests {
tweak { tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS) } 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" this `fails requirement` "the amounts balance"
} }
@ -206,7 +206,7 @@ class CashTests {
this `fails requirement` "required contracts.Cash.Commands.Move command" this `fails requirement` "required contracts.Cash.Commands.Move command"
tweak { tweak {
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.accepts() 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).editInstitution(MINI_CORP) }
output { inState.copy(amount = inState.amount - 200.DOLLARS) } 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" this `fails requirement` "at issuer MegaCorp the amounts balance"
@ -253,7 +253,7 @@ class CashTests {
// This works. // This works.
output { inState.copy(owner = DUMMY_PUBKEY_2) } output { inState.copy(owner = DUMMY_PUBKEY_2) }
output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) } 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() this.accepts()
} }
@ -272,7 +272,7 @@ class CashTests {
input { pounds } input { pounds }
output { inState `owned by` DUMMY_PUBKEY_2 } output { inState `owned by` DUMMY_PUBKEY_2 }
output { pounds `owned by` DUMMY_PUBKEY_1 } 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() this.accepts()
} }

View File

@ -30,7 +30,7 @@ class CommercialPaperTests {
transactionGroup { transactionGroup {
transaction { transaction {
output { PAPER_1 } output { PAPER_1 }
arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue } arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
} }
expectFailureOfTx(1, "signed by the claimed issuer") expectFailureOfTx(1, "signed by the claimed issuer")
@ -42,7 +42,7 @@ class CommercialPaperTests {
transactionGroup { transactionGroup {
transaction { transaction {
output { PAPER_1.copy(faceValue = 0.DOLLARS) } 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") expectFailureOfTx(1, "face value is not zero")
@ -54,7 +54,7 @@ class CommercialPaperTests {
transactionGroup { transactionGroup {
transaction { transaction {
output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) } 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") expectFailureOfTx(1, "maturity date is not in the past")
@ -70,7 +70,7 @@ class CommercialPaperTests {
transaction { transaction {
input("paper") input("paper")
output { PAPER_1 } output { PAPER_1 }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
} }
expectFailureOfTx(1, "there is no input state") expectFailureOfTx(1, "there is no input state")
@ -101,7 +101,7 @@ class CommercialPaperTests {
// Some CP is issued onto the ledger by MegaCorp. // Some CP is issued onto the ledger by MegaCorp.
transaction { transaction {
output("paper") { PAPER_1 } 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, // 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") input("alice's $900")
output { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY } output { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
output("alice's paper") { "paper".output `owned by` ALICE } output("alice's paper") { "paper".output `owned by` ALICE }
arg(ALICE) { Cash.Commands.Move } arg(ALICE) { Cash.Commands.Move() }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.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 // 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) if (!destroyPaperAtRedemption)
output { "paper".output } output { "paper".output }
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move } arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
arg(ALICE) { CommercialPaper.Commands.Redeem } arg(ALICE) { CommercialPaper.Commands.Redeem() }
} }
} }
} }

View File

@ -68,7 +68,7 @@ class CrowdFundTests {
pledgeTotal = CF_1.pledgeTotal + 1000.DOLLARS pledgeTotal = CF_1.pledgeTotal + 1000.DOLLARS
) } ) }
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY } output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
arg(ALICE) { Cash.Commands.Move } arg(ALICE) { Cash.Commands.Move() }
arg(ALICE) { CrowdFund.Commands.Fund } arg(ALICE) { CrowdFund.Commands.Fund }
} }

View File

@ -20,12 +20,12 @@ class TransactionGroupTests {
transaction { transaction {
input("£1000") input("£1000")
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE } 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 { transaction {
input("alice's £1000") input("alice's £1000")
arg(ALICE) { Cash.Commands.Move } arg(ALICE) { Cash.Commands.Move() }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) } 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 val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB
output { HALF } output { HALF }
output { HALF } output { HALF }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move } arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
} }
verify() verify()
@ -57,7 +57,7 @@ class TransactionGroupTests {
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE
output { HALF } output { HALF }
output { HALF } output { HALF }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move } arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
} }
assertNotEquals(conflict1, conflict2) assertNotEquals(conflict1, conflict2)
@ -89,7 +89,7 @@ class TransactionGroupTests {
// points nowhere. // points nowhere.
val ref = ContractStateRef(SecureHash.randomSHA256(), 0) val ref = ContractStateRef(SecureHash.randomSHA256(), 0)
tg.txns.add(LedgerTransaction( 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) { val e = assertFailsWith(TransactionResolutionException::class) {
@ -110,7 +110,7 @@ class TransactionGroupTests {
input("£1000") input("£1000")
input("£1000") input("£1000")
output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) } 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) { assertFailsWith(TransactionConflictException::class) {

View File

@ -2,13 +2,13 @@ package core.serialization
import contracts.Cash import contracts.Cash
import core.* import core.*
import core.testutils.DUMMY_PUBKEY_1 import core.testutils.*
import core.testutils.MINI_CORP
import core.testutils.TestUtils
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.SignatureException import java.security.SignatureException
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertNull
class TransactionSerializationTests { class TransactionSerializationTests {
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change). // Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
@ -23,7 +23,7 @@ class TransactionSerializationTests {
@Before @Before
fun setup() { fun setup() {
tx = PartialTransaction( 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. // If the signature was replaced in transit, we don't like it.
assertFailsWith(SignatureException::class) { assertFailsWith(SignatureException::class) {
val tx2 = PartialTransaction(fakeStateRef, outputState, changeState, 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) tx2.signWith(TestUtils.keypair2)
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify() 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)
}
} }

View File

@ -2,8 +2,13 @@
package core.testutils package core.testutils
import com.google.common.io.BaseEncoding
import contracts.* import contracts.*
import core.* 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.KeyPairGenerator
import java.security.PublicKey import java.security.PublicKey
import java.time.Instant import java.time.Instant
@ -48,6 +53,33 @@ val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
DUMMY_PROGRAM_ID to DummyContract 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. // Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.