mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merge branch 'finish-timestamping'
# Conflicts: # docs/build/html/searchindex.js # src/core/serialization/Kryo.kt
This commit is contained in:
commit
b8a50a65cf
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,8 +72,9 @@ class CrowdFund : Contract {
|
||||
val inputCash: List<Cash.State> = tx.inStates.filterIsInstance<Cash.State>()
|
||||
val pledge = outputCrowdFund.pledges.last()
|
||||
val pledgedCash = outputCash.single()
|
||||
val time = tx.time ?: throw IllegalStateException("Transaction must be timestamped")
|
||||
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))
|
||||
"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)
|
||||
@ -102,8 +103,10 @@ class CrowdFund : Contract {
|
||||
return true
|
||||
}
|
||||
|
||||
val time = tx.time ?: throw IllegalStateException("Transaction must be timestamped")
|
||||
|
||||
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 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))
|
||||
@ -123,8 +126,9 @@ class CrowdFund : Contract {
|
||||
|
||||
is Commands.Funded -> {
|
||||
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
|
||||
requireThat {
|
||||
"the closing date has past" by (tx.time >= outputCrowdFund.closingTime)
|
||||
val time = tx.time ?: throw IllegalStateException("Transaction must be timestamped")
|
||||
requireThat {
|
||||
"the closing date has past" by (time >= outputCrowdFund.closingTime)
|
||||
"the input has an open state" by (!inputCrowdFund.closed)
|
||||
"the output registration has a closed state" by (outputCrowdFund.closed)
|
||||
// TODO how to simplify the boilerplate associated with unchanged elements
|
||||
|
@ -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())
|
||||
|
@ -13,6 +13,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
|
||||
@ -210,6 +211,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>()
|
||||
@ -221,17 +223,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)
|
||||
// Added for
|
||||
register(CommercialPaper.Commands.Move::class.java)
|
||||
register(CommercialPaper.Commands.Redeem::class.java)
|
||||
register(CommercialPaper.Commands.Issue::class.java)
|
||||
registerDataClass<CrowdFund.State>()
|
||||
registerDataClass<CrowdFund.Pledge>()
|
||||
register(CrowdFund.Commands.Register.javaClass)
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ class CrowdFundTests {
|
||||
pledgeTotal = CF_1.pledgeTotal + 1000.DOLLARS
|
||||
) }
|
||||
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
|
||||
arg(ALICE) { Cash.Commands.Move }
|
||||
arg(ALICE) { Cash.Commands.Move() }
|
||||
arg(ALICE) { CrowdFund.Commands.Fund }
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
@ -48,6 +53,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