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:
kasiastreich 2016-12-19 13:14:36 +00:00 committed by GitHub
parent 6f3ed327a0
commit 4ffad426c1

View File

@ -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;
}); });