mirror of
https://github.com/corda/corda.git
synced 2025-02-20 17:33:15 +00:00
Writing contract tests docs (#17)
* Change tutorial-test-dsl to cover CommercialPaper instead of Cash contract. * Address PR comments. * Add Java code examples. * Minor fixes. * Add double spend example to the tutorial. * Small grammar fixes for writing a contract test tutorial.
This commit is contained in:
parent
6f3ed327a0
commit
4ffad426c1
@ -1,6 +1,6 @@
|
|||||||
.. highlight:: kotlin
|
.. highlight:: kotlin
|
||||||
.. role:: kotlin(code)
|
.. role:: kotlin(code)
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
.. raw:: html
|
.. raw:: html
|
||||||
|
|
||||||
|
|
||||||
@ -10,7 +10,7 @@
|
|||||||
Writing a contract test
|
Writing a contract test
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
This tutorial will take you through the steps required to write a contract test using Kotlin and/or Java.
|
This tutorial will take you through the steps required to write a contract test using Kotlin and Java.
|
||||||
|
|
||||||
The testing DSL allows one to define a piece of the ledger with transactions referring to each other, and ways of
|
The testing DSL allows one to define a piece of the ledger with transactions referring to each other, and ways of
|
||||||
verifying their correctness.
|
verifying their correctness.
|
||||||
@ -24,10 +24,13 @@ We start with the empty ledger:
|
|||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Test
|
class CommercialPaperTest{
|
||||||
fun emptyLedger() {
|
@Test
|
||||||
ledger {
|
fun emptyLedger() {
|
||||||
|
ledger {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
@ -45,18 +48,43 @@ We start with the empty ledger:
|
|||||||
The DSL keyword ``ledger`` takes a closure that can build up several transactions and may verify their overall
|
The DSL keyword ``ledger`` takes a closure that can build up several transactions and may verify their overall
|
||||||
correctness. A ledger is effectively a fresh world with no pre-existing transactions or services within it.
|
correctness. A ledger is effectively a fresh world with no pre-existing transactions or services within it.
|
||||||
|
|
||||||
Let's add a Cash transaction:
|
We will start with defining helper function that returns a ``CommercialPaper`` state:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
fun getPaper(): ICommercialPaperState = CommercialPaper.State(
|
||||||
|
issuance = MEGA_CORP.ref(123),
|
||||||
|
owner = MEGA_CORP_PUBKEY,
|
||||||
|
faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
|
||||||
|
maturityDate = TEST_TX_TIME + 7.days
|
||||||
|
)
|
||||||
|
|
||||||
|
.. sourcecode:: java
|
||||||
|
|
||||||
|
private final OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{123});
|
||||||
|
|
||||||
|
private ICommercialPaperState getPaper() {
|
||||||
|
return new JavaCommercialPaper.State(
|
||||||
|
getMEGA_CORP().ref(defaultRef),
|
||||||
|
getMEGA_CORP_PUBKEY(),
|
||||||
|
issuedBy(DOLLARS(1000), getMEGA_CORP().ref(defaultRef)),
|
||||||
|
getTEST_TX_TIME().plus(7, ChronoUnit.DAYS)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
It's a ``CommercialPaper`` issued by ``MEGA_CORP`` with face value of $1000 and maturity date in 7 days.
|
||||||
|
|
||||||
|
Let's add a ``CommercialPaper`` transaction:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun simpleCashDoesntCompile() {
|
fun simpleCPDoesntCompile() {
|
||||||
val inState = Cash.State(
|
val inState = getPaper()
|
||||||
amount = 1000.DOLLARS `issued by` DUMMY_CASH_ISSUER,
|
|
||||||
owner = DUMMY_PUBKEY_1
|
|
||||||
)
|
|
||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
input(inState)
|
input(inState)
|
||||||
@ -67,11 +95,8 @@ Let's add a Cash transaction:
|
|||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void simpleCashDoesntCompile() {
|
public void simpleCPDoesntCompile() {
|
||||||
Cash.State inState = new Cash.State(
|
ICommercialPaperState inState = getPaper();
|
||||||
issuedBy(DOLLARS(1000), getDUMMY_CASH_ISSUER()),
|
|
||||||
getDUMMY_PUBKEY_1()
|
|
||||||
);
|
|
||||||
ledger(l -> {
|
ledger(l -> {
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.input(inState);
|
tx.input(inState);
|
||||||
@ -83,7 +108,7 @@ Let's add a Cash transaction:
|
|||||||
We can add a transaction to the ledger using the ``transaction`` primitive. The transaction in turn may be defined by
|
We can add a transaction to the ledger using the ``transaction`` primitive. The transaction in turn may be defined by
|
||||||
specifying ``input``-s, ``output``-s, ``command``-s and ``attachment``-s.
|
specifying ``input``-s, ``output``-s, ``command``-s and ``attachment``-s.
|
||||||
|
|
||||||
The above ``input`` call is a bit special: Transactions don't actually contain input states, just references
|
The above ``input`` call is a bit special; transactions don't actually contain input states, just references
|
||||||
to output states of other transactions. Under the hood the above ``input`` call creates a dummy transaction in the
|
to output states of other transactions. Under the hood the above ``input`` call creates a dummy transaction in the
|
||||||
ledger (that won't be verified) which outputs the specified state, and references that from this transaction.
|
ledger (that won't be verified) which outputs the specified state, and references that from this transaction.
|
||||||
|
|
||||||
@ -93,11 +118,11 @@ The above code however doesn't compile:
|
|||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
Error:(26, 21) Kotlin: Type mismatch: inferred type is Unit but EnforceVerifyOrFail was expected
|
Error:(29, 17) Kotlin: Type mismatch: inferred type is Unit but EnforceVerifyOrFail was expected
|
||||||
|
|
||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
Error:(26, 31) java: incompatible types: bad return type in lambda expression missing return value
|
Error:(35, 27) java: incompatible types: bad return type in lambda expression missing return value
|
||||||
|
|
||||||
This is deliberate: The DSL forces us to specify either ``this.verifies()`` or ``this `fails with` "some text"`` on the
|
This is deliberate: The DSL forces us to specify either ``this.verifies()`` or ``this `fails with` "some text"`` on the
|
||||||
last line of ``transaction``:
|
last line of ``transaction``:
|
||||||
@ -107,11 +132,8 @@ last line of ``transaction``:
|
|||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun simpleCash() {
|
fun simpleCP() {
|
||||||
val inState = Cash.State(
|
val inState = getPaper()
|
||||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
|
||||||
owner = DUMMY_PUBKEY_1
|
|
||||||
)
|
|
||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
input(inState)
|
input(inState)
|
||||||
@ -123,11 +145,8 @@ last line of ``transaction``:
|
|||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void simpleCash() {
|
public void simpleCP() {
|
||||||
Cash.State inState = new Cash.State(
|
ICommercialPaperState inState = getPaper();
|
||||||
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
|
|
||||||
getDUMMY_PUBKEY_1()
|
|
||||||
);
|
|
||||||
ledger(l -> {
|
ledger(l -> {
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.input(inState);
|
tx.input(inState);
|
||||||
@ -137,30 +156,20 @@ last line of ``transaction``:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
The code finally compiles. When run, it produces the following error::
|
Let's take a look at a transaction that fails.
|
||||||
|
|
||||||
net.corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalArgumentException: Failed requirement: for deposit [01] at issuer Snake Oil Issuer the amounts balance
|
|
||||||
|
|
||||||
.. note:: The reference here to the 'Snake Oil Issuer' is because we are using the pre-canned ``DUMMY_CASH_ISSUER``
|
|
||||||
identity as the issuer of our cash.
|
|
||||||
|
|
||||||
The transaction verification failed, because the sum of inputs does not equal the sum of outputs. We can specify that
|
|
||||||
this is intended behaviour by changing ``this.verifies()`` to ``this `fails with` "the amounts balance"``:
|
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun simpleCashFailsWith() {
|
fun simpleCPMove() {
|
||||||
val inState = Cash.State(
|
val inState = getPaper()
|
||||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
|
||||||
owner = DUMMY_PUBKEY_1
|
|
||||||
)
|
|
||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
input(inState)
|
input(inState)
|
||||||
this `fails with` "the amounts balance"
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,15 +177,59 @@ this is intended behaviour by changing ``this.verifies()`` to ``this `fails with
|
|||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void simpleCashFailsWith() {
|
public void simpleCPMove() {
|
||||||
Cash.State inState = new Cash.State(
|
ICommercialPaperState inState = getPaper();
|
||||||
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
|
|
||||||
getDUMMY_PUBKEY_1()
|
|
||||||
);
|
|
||||||
ledger(l -> {
|
ledger(l -> {
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.input(inState);
|
tx.input(inState);
|
||||||
return tx.failsWith("the amounts balance");
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
|
||||||
|
return tx.verifies();
|
||||||
|
});
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
When run, that code produces the following error:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
net.corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalArgumentException: Failed requirement: the state is propagated
|
||||||
|
|
||||||
|
.. sourcecode:: java
|
||||||
|
|
||||||
|
net.corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalStateException: the state is propagated
|
||||||
|
|
||||||
|
The transaction verification failed, because we wanted to move paper but didn't specify an output - but the state should be propagated.
|
||||||
|
However we can specify that this is an intended behaviour by changing ``this.verifies()`` to ``this `fails with` "the state is propagated"``:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun simpleCPMoveFails() {
|
||||||
|
val inState = getPaper()
|
||||||
|
ledger {
|
||||||
|
transaction {
|
||||||
|
input(inState)
|
||||||
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||||
|
this `fails with` "the state is propagated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.. sourcecode:: java
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void simpleCPMoveFails() {
|
||||||
|
ICommercialPaperState inState = getPaper();
|
||||||
|
ledger(l -> {
|
||||||
|
l.transaction(tx -> {
|
||||||
|
tx.input(inState);
|
||||||
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
|
||||||
|
return tx.failsWith("the state is propagated");
|
||||||
});
|
});
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
@ -189,17 +242,14 @@ We can continue to build the transaction until it ``verifies``:
|
|||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun simpleCashSuccess() {
|
fun simpleCPMoveSuccess() {
|
||||||
val inState = Cash.State(
|
val inState = getPaper()
|
||||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
|
||||||
owner = DUMMY_PUBKEY_1
|
|
||||||
)
|
|
||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
input(inState)
|
input(inState)
|
||||||
this `fails with` "the amounts balance"
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||||
output(inState.copy(owner = DUMMY_PUBKEY_2))
|
this `fails with` "the state is propagated"
|
||||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
output("alice's paper") { inState `owned by` ALICE_PUBKEY }
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,55 +258,45 @@ We can continue to build the transaction until it ``verifies``:
|
|||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void simpleCashSuccess() {
|
public void simpleCPMoveSuccess() {
|
||||||
Cash.State inState = new Cash.State(
|
ICommercialPaperState inState = getPaper();
|
||||||
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
|
|
||||||
getDUMMY_PUBKEY_1()
|
|
||||||
);
|
|
||||||
ledger(l -> {
|
ledger(l -> {
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.input(inState);
|
tx.input(inState);
|
||||||
tx.failsWith("the amounts balance");
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
|
||||||
tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2()));
|
tx.failsWith("the state is propagated");
|
||||||
tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
|
tx.output("alice's paper", inState.withOwner(getALICE_PUBKEY()));
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
``output`` specifies that we want the input state to be transferred to ``DUMMY_PUBKEY_2`` and ``command`` adds the
|
``output`` specifies that we want the input state to be transferred to ``ALICE`` and ``command`` adds the
|
||||||
``Move`` command itself, signed by the current owner of the input state, ``DUMMY_PUBKEY_1``.
|
``Move`` command itself, signed by the current owner of the input state, ``MEGA_CORP_PUBKEY``.
|
||||||
|
|
||||||
We constructed a complete signed cash transaction from ``DUMMY_PUBKEY_1`` to ``DUMMY_PUBKEY_2`` and verified it. Note
|
We constructed a complete signed commercial paper transaction and verified it. Note how we left in the ``fails with``
|
||||||
how we left in the ``fails with`` line - this is fine, the failure will be tested on the partially constructed
|
line - this is fine, the failure will be tested on the partially constructed transaction.
|
||||||
transaction.
|
|
||||||
|
|
||||||
What should we do if we wanted to test what happens when the wrong party signs the transaction? If we simply add a
|
What should we do if we wanted to test what happens when the wrong party signs the transaction? If we simply add a
|
||||||
``command`` it will ruin the transaction for good... Enter ``tweak``:
|
``command`` it will permanently ruin the transaction... Enter ``tweak``:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun simpleCashTweakSuccess() {
|
fun `simple issuance with tweak`() {
|
||||||
val inState = Cash.State(
|
|
||||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
|
||||||
owner = DUMMY_PUBKEY_1
|
|
||||||
)
|
|
||||||
ledger {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
input(inState)
|
output("paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp.
|
||||||
this `fails with` "the amounts balance"
|
|
||||||
output(inState.copy(owner = DUMMY_PUBKEY_2))
|
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
command(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
|
||||||
this `fails with` "the owning keys are the same as the signing keys"
|
timestamp(TEST_TX_TIME)
|
||||||
|
this `fails with` "output states are issued by a command signer"
|
||||||
}
|
}
|
||||||
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
timestamp(TEST_TX_TIME)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -265,33 +305,29 @@ What should we do if we wanted to test what happens when the wrong party signs t
|
|||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void simpleCashTweakSuccess() {
|
public void simpleIssuanceWithTweak() {
|
||||||
Cash.State inState = new Cash.State(
|
|
||||||
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
|
|
||||||
getDUMMY_PUBKEY_1()
|
|
||||||
);
|
|
||||||
ledger(l -> {
|
ledger(l -> {
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.input(inState);
|
tx.output("paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
|
||||||
tx.failsWith("the amounts balance");
|
|
||||||
tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2()));
|
|
||||||
|
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
tw.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move());
|
tw.command(getDUMMY_PUBKEY_1(), new JavaCommercialPaper.Commands.Issue());
|
||||||
return tw.failsWith("the owning keys are the same as the signing keys");
|
tw.timestamp(getTEST_TX_TIME());
|
||||||
|
return tw.failsWith("output states are issued by a command signer");
|
||||||
});
|
});
|
||||||
tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue());
|
||||||
|
tx.timestamp(getTEST_TX_TIME());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
``tweak`` creates a local copy of the transaction. This allows the local "ruining" of the transaction allowing testing
|
|
||||||
of different error conditions.
|
``tweak`` creates a local copy of the transaction. This makes possible to locally "ruin" the transaction while not
|
||||||
|
modifying the original one, allowing testing of different error conditions.
|
||||||
|
|
||||||
We now have a neat little test that tests a single transaction. This is already useful, and in fact testing of a single
|
We now have a neat little test that tests a single transaction. This is already useful, and in fact testing of a single
|
||||||
transaction in this way is very common. There is even a shorthand toplevel ``transaction`` primitive that creates a
|
transaction in this way is very common. There is even a shorthand top-level ``transaction`` primitive that creates a
|
||||||
ledger with a single transaction:
|
ledger with a single transaction:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
@ -299,22 +335,16 @@ ledger with a single transaction:
|
|||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun simpleCashTweakSuccessTopLevelTransaction() {
|
fun `simple issuance with tweak and top level transaction`() {
|
||||||
val inState = Cash.State(
|
|
||||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
|
||||||
owner = DUMMY_PUBKEY_1
|
|
||||||
)
|
|
||||||
transaction {
|
transaction {
|
||||||
input(inState)
|
output("paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp.
|
||||||
this `fails with` "the amounts balance"
|
|
||||||
output(inState.copy(owner = DUMMY_PUBKEY_2))
|
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
command(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
|
||||||
this `fails with` "the owning keys are the same as the signing keys"
|
timestamp(TEST_TX_TIME)
|
||||||
|
this `fails with` "output states are issued by a command signer"
|
||||||
}
|
}
|
||||||
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
timestamp(TEST_TX_TIME)
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -322,21 +352,16 @@ ledger with a single transaction:
|
|||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void simpleCashTweakSuccessTopLevelTransaction() {
|
public void simpleIssuanceWithTweakTopLevelTx() {
|
||||||
Cash.State inState = new Cash.State(
|
|
||||||
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
|
|
||||||
getDUMMY_PUBKEY_1()
|
|
||||||
);
|
|
||||||
transaction(tx -> {
|
transaction(tx -> {
|
||||||
tx.input(inState);
|
tx.output("paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
|
||||||
tx.failsWith("the amounts balance");
|
|
||||||
tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2()));
|
|
||||||
|
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
tw.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move());
|
tw.command(getDUMMY_PUBKEY_1(), new JavaCommercialPaper.Commands.Issue());
|
||||||
return tw.failsWith("the owning keys are the same as the signing keys");
|
tw.timestamp(getTEST_TX_TIME());
|
||||||
|
return tw.failsWith("output states are issued by a command signer");
|
||||||
});
|
});
|
||||||
tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue());
|
||||||
|
tx.timestamp(getTEST_TX_TIME());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -351,21 +376,30 @@ Now that we know how to define a single transaction, let's look at how to define
|
|||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun chainCash() {
|
fun `chain commercial paper`() {
|
||||||
|
val issuer = MEGA_CORP.ref(123)
|
||||||
|
|
||||||
ledger {
|
ledger {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
output("MEGA_CORP cash") {
|
output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY)
|
||||||
Cash.State(
|
|
||||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
|
||||||
owner = MEGA_CORP_PUBKEY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
input("MEGA_CORP cash")
|
transaction("Issuance") {
|
||||||
output("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_1))
|
output("paper") { getPaper() }
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
transaction("Trade") {
|
||||||
|
input("paper")
|
||||||
|
input("alice's $900")
|
||||||
|
output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
|
||||||
|
output("alice's paper") { "paper".output<ICommercialPaperState>() `owned by` ALICE_PUBKEY }
|
||||||
|
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||||
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -374,141 +408,176 @@ Now that we know how to define a single transaction, let's look at how to define
|
|||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void chainCash() {
|
public void chainCommercialPaper() {
|
||||||
|
PartyAndReference issuer = getMEGA_CORP().ref(defaultRef);
|
||||||
ledger(l -> {
|
ledger(l -> {
|
||||||
l.unverifiedTransaction(tx -> {
|
l.unverifiedTransaction(tx -> {
|
||||||
tx.output("MEGA_CORP cash",
|
tx.output("alice's $900",
|
||||||
new Cash.State(
|
new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null));
|
||||||
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
|
return Unit.INSTANCE;
|
||||||
getMEGA_CORP_PUBKEY()
|
});
|
||||||
)
|
|
||||||
);
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
});
|
|
||||||
|
|
||||||
l.transaction(tx -> {
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
tx.input("MEGA_CORP cash");
|
l.transaction("Issuance", tx -> {
|
||||||
Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash");
|
tx.output("paper", getPaper());
|
||||||
tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1()));
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue());
|
||||||
tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
tx.timestamp(getTEST_TX_TIME());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
l.transaction("Trade", tx -> {
|
||||||
|
tx.input("paper");
|
||||||
|
tx.input("alice's $900");
|
||||||
|
tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null));
|
||||||
|
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||||
|
tx.output("alice's paper", inputPaper.withOwner(getALICE_PUBKEY()));
|
||||||
|
tx.command(getALICE_PUBKEY(), new Cash.Commands.Move());
|
||||||
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
|
||||||
|
return tx.verifies();
|
||||||
|
});
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
In this example we declare that ``MEGA_CORP`` has a thousand dollars but we don't care where from, for this we can use
|
|
||||||
|
In this example we declare that ``ALICE`` has $900 but we don't care where from. For this we can use
|
||||||
``unverifiedTransaction``. Note how we don't need to specify ``this.verifies()``.
|
``unverifiedTransaction``. Note how we don't need to specify ``this.verifies()``.
|
||||||
|
|
||||||
The ``output`` cash was labelled with ``"MEGA_CORP cash"``, we can subsequently referred to this other transactions, e.g.
|
Notice that we labelled output with ``"alice's $900"``, also in transaction named ``"Issuance"``
|
||||||
by ``input("MEGA_CORP cash")`` or ``"MEGA_CORP cash".output<Cash.State>()``.
|
we labelled a commercial paper with ``"paper"``. Now we can subsequently refer to them in other transactions, e.g.
|
||||||
|
by ``input("alice's $900")`` or ``"paper".output<ICommercialPaperState>()``.
|
||||||
|
|
||||||
What happens if we reuse the output cash twice?
|
The last transaction named ``"Trade"`` exemplifies simple fact of selling the ``CommercialPaper`` to Alice for her $900,
|
||||||
|
$100 less than the face value at 10% interest after only 7 days.
|
||||||
|
|
||||||
|
We can also test whole ledger calling ``this.verifies()`` and ``this.fails()`` on the ledger level.
|
||||||
|
To do so let's create a simple example that uses the same input twice:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun chainCashDoubleSpend() {
|
fun `chain commercial paper double spend`() {
|
||||||
|
val issuer = MEGA_CORP.ref(123)
|
||||||
ledger {
|
ledger {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
output("MEGA_CORP cash") {
|
output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY)
|
||||||
Cash.State(
|
|
||||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
|
||||||
owner = MEGA_CORP_PUBKEY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
input("MEGA_CORP cash")
|
transaction("Issuance") {
|
||||||
output("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_1))
|
output("paper") { getPaper() }
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction("Trade") {
|
||||||
|
input("paper")
|
||||||
|
input("alice's $900")
|
||||||
|
output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
|
||||||
|
output("alice's paper") { "paper".output<ICommercialPaperState>() `owned by` ALICE_PUBKEY }
|
||||||
|
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||||
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
input("MEGA_CORP cash")
|
input("paper")
|
||||||
// We send it to another pubkey so that the transaction is not identical to the previous one
|
// We moved a paper to another pubkey.
|
||||||
output("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_2))
|
output("bob's paper") { "paper".output<ICommercialPaperState>() `owned by` BOB_PUBKEY }
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.fails()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void chainCashDoubleSpend() {
|
public void chainCommercialPaperDoubleSpend() {
|
||||||
|
PartyAndReference issuer = getMEGA_CORP().ref(defaultRef);
|
||||||
ledger(l -> {
|
ledger(l -> {
|
||||||
l.unverifiedTransaction(tx -> {
|
l.unverifiedTransaction(tx -> {
|
||||||
tx.output("MEGA_CORP cash",
|
tx.output("alice's $900",
|
||||||
new Cash.State(
|
new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null));
|
||||||
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
|
|
||||||
getMEGA_CORP_PUBKEY()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
|
|
||||||
l.transaction(tx -> {
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
tx.input("MEGA_CORP cash");
|
l.transaction("Issuance", tx -> {
|
||||||
Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash");
|
tx.output("paper", getPaper());
|
||||||
tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1()));
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue());
|
||||||
tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
tx.timestamp(getTEST_TX_TIME());
|
||||||
|
return tx.verifies();
|
||||||
|
});
|
||||||
|
|
||||||
|
l.transaction("Trade", tx -> {
|
||||||
|
tx.input("paper");
|
||||||
|
tx.input("alice's $900");
|
||||||
|
tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null));
|
||||||
|
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||||
|
tx.output("alice's paper", inputPaper.withOwner(getALICE_PUBKEY()));
|
||||||
|
tx.command(getALICE_PUBKEY(), new Cash.Commands.Move());
|
||||||
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
|
|
||||||
l.transaction(tx -> {
|
l.transaction(tx -> {
|
||||||
tx.input("MEGA_CORP cash");
|
tx.input("paper");
|
||||||
Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash");
|
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||||
// We send it to another pubkey so that the transaction is not identical to the previous one
|
// We moved a paper to other pubkey.
|
||||||
tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_2()));
|
tx.output("bob's paper", inputPaper.withOwner(getBOB_PUBKEY()));
|
||||||
tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
|
l.fails();
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
The transactions ``verifies()`` individually, however the state was spent twice!
|
The transactions ``verifies()`` individually, however the state was spent twice! That's why we need the global ledger
|
||||||
|
verification (``this.fails()`` at the end). As in previous examples we can use ``tweak`` to create a local copy of the whole ledger:
|
||||||
We can also verify the complete ledger by calling ``verifies``/``fails`` on the ledger level. We can also use
|
|
||||||
``tweak`` to create a local copy of the whole ledger:
|
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun chainCashDoubleSpendFailsWith() {
|
fun `chain commercial tweak`() {
|
||||||
|
val issuer = MEGA_CORP.ref(123)
|
||||||
ledger {
|
ledger {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
output("MEGA_CORP cash") {
|
output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY)
|
||||||
Cash.State(
|
|
||||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
|
||||||
owner = MEGA_CORP_PUBKEY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
input("MEGA_CORP cash")
|
transaction("Issuance") {
|
||||||
output("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_1))
|
output("paper") { getPaper() }
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction("Trade") {
|
||||||
|
input("paper")
|
||||||
|
input("alice's $900")
|
||||||
|
output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
|
||||||
|
output("alice's paper") { "paper".output<ICommercialPaperState>() `owned by` ALICE_PUBKEY }
|
||||||
|
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||||
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
transaction {
|
transaction {
|
||||||
input("MEGA_CORP cash")
|
input("paper")
|
||||||
// We send it to another pubkey so that the transaction is not identical to the previous one
|
// We moved a paper to another pubkey.
|
||||||
output("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_1))
|
output("bob's paper") { "paper".output<ICommercialPaperState>() `owned by` BOB_PUBKEY }
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
this.fails()
|
this.fails()
|
||||||
@ -521,39 +590,46 @@ We can also verify the complete ledger by calling ``verifies``/``fails`` on the
|
|||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void chainCashDoubleSpendFailsWith() {
|
public void chainCommercialPaperTweak() {
|
||||||
|
PartyAndReference issuer = getMEGA_CORP().ref(defaultRef);
|
||||||
ledger(l -> {
|
ledger(l -> {
|
||||||
l.unverifiedTransaction(tx -> {
|
l.unverifiedTransaction(tx -> {
|
||||||
tx.output("MEGA_CORP cash",
|
tx.output("alice's $900",
|
||||||
new Cash.State(
|
new Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null));
|
||||||
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
|
|
||||||
getMEGA_CORP_PUBKEY()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
|
|
||||||
l.transaction(tx -> {
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
tx.input("MEGA_CORP cash");
|
l.transaction("Issuance", tx -> {
|
||||||
Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash");
|
tx.output("paper", getPaper());
|
||||||
tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1()));
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue());
|
||||||
tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
tx.timestamp(getTEST_TX_TIME());
|
||||||
|
return tx.verifies();
|
||||||
|
});
|
||||||
|
|
||||||
|
l.transaction("Trade", tx -> {
|
||||||
|
tx.input("paper");
|
||||||
|
tx.input("alice's $900");
|
||||||
|
tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null));
|
||||||
|
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||||
|
tx.output("alice's paper", inputPaper.withOwner(getALICE_PUBKEY()));
|
||||||
|
tx.command(getALICE_PUBKEY(), new Cash.Commands.Move());
|
||||||
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
|
|
||||||
l.tweak(lw -> {
|
l.tweak(lw -> {
|
||||||
lw.transaction(tx -> {
|
lw.transaction(tx -> {
|
||||||
tx.input("MEGA_CORP cash");
|
tx.input("paper");
|
||||||
Cash.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash");
|
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||||
// We send it to another pubkey so that the transaction is not identical to the previous one
|
// We moved a paper to another pubkey.
|
||||||
tx.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_2()));
|
tx.output("bob's paper", inputPaper.withOwner(getBOB_PUBKEY()));
|
||||||
tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
|
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
|
||||||
return tx.verifies();
|
return tx.verifies();
|
||||||
});
|
});
|
||||||
lw.fails();
|
lw.fails();
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
|
|
||||||
l.verifies();
|
l.verifies();
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user