* 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.
24 KiB
Writing a contract test
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 verifying their correctness.
Testing single transactions
We start with the empty ledger:
class CommercialPaperTest{
@Test
fun emptyLedger() {
{
ledger }
}
...
}
import static net.corda.core.testing.JavaTestHelpers.*;
import static net.corda.core.contracts.JavaTestHelpers.*;
@Test
public void emptyLedger() {
ledger(l -> {
return Unit.INSTANCE; // We need to return this explicitly
});
}
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.
We will start with defining helper function that returns a
CommercialPaper
state:
fun getPaper(): ICommercialPaperState = CommercialPaper.State(
= MEGA_CORP.ref(123),
issuance = MEGA_CORP_PUBKEY,
owner = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
faceValue = TEST_TX_TIME + 7.days
maturityDate )
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:
@Test
fun simpleCPDoesntCompile() {
val inState = getPaper()
{
ledger {
transaction (inState)
input}
}
}
@Test
public void simpleCPDoesntCompile() {
= getPaper();
ICommercialPaperState inState ledger(l -> {
.transaction(tx -> {
l.input(inState);
tx});
return Unit.INSTANCE;
});
}
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.
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 ledger (that won't be verified) which
outputs the specified state, and references that from this
transaction.
The above code however doesn't compile:
:(29, 17) Kotlin: Type mismatch: inferred type is Unit but EnforceVerifyOrFail was expected Error
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 last line of
transaction
:
@Test
fun simpleCP() {
val inState = getPaper()
{
ledger {
transaction (inState)
inputthis.verifies()
}
}
}
@Test
public void simpleCP() {
= getPaper();
ICommercialPaperState inState ledger(l -> {
.transaction(tx -> {
l.input(inState);
txreturn tx.verifies();
});
return Unit.INSTANCE;
});
}
Let's take a look at a transaction that fails.
@Test
fun simpleCPMove() {
val inState = getPaper()
{
ledger {
transaction (inState)
input(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
commandthis.verifies()
}
}
}
@Test
public void simpleCPMove() {
= getPaper();
ICommercialPaperState inState ledger(l -> {
.transaction(tx -> {
l.input(inState);
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
txreturn tx.verifies();
});
return Unit.INSTANCE;
});
}
When run, that code produces the following error:
.corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalArgumentException: Failed requirement: the state is propagated net
.corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalStateException: the state is propagated net
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"
:
@Test
fun simpleCPMoveFails() {
val inState = getPaper()
{
ledger {
transaction (inState)
input(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
commandthis `fails with` "the state is propagated"
}
}
}
@Test
public void simpleCPMoveFails() {
= getPaper();
ICommercialPaperState inState ledger(l -> {
.transaction(tx -> {
l.input(inState);
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
txreturn tx.failsWith("the state is propagated");
});
return Unit.INSTANCE;
});
}
We can continue to build the transaction until it
verifies
:
@Test
fun simpleCPMoveSuccess() {
val inState = getPaper()
{
ledger {
transaction (inState)
input(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
commandthis `fails with` "the state is propagated"
("alice's paper") { inState `owned by` ALICE_PUBKEY }
outputthis.verifies()
}
}
}
@Test
public void simpleCPMoveSuccess() {
= getPaper();
ICommercialPaperState inState ledger(l -> {
.transaction(tx -> {
l.input(inState);
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
tx.failsWith("the state is propagated");
tx.output("alice's paper", inState.withOwner(getALICE_PUBKEY()));
txreturn tx.verifies();
});
return Unit.INSTANCE;
});
}
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, MEGA_CORP_PUBKEY
.
We constructed a complete signed commercial paper transaction and
verified it. Note how we left in the fails with
line - this
is fine, the failure will be tested on the partially constructed
transaction.
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 permanently ruin the transaction... Enter tweak
:
@Test
fun `simple issuance with tweak`() {
{
ledger {
transaction ("paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp.
output{
tweak (DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
command(TEST_TX_TIME)
timestampthis `fails with` "output states are issued by a command signer"
}
(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
command(TEST_TX_TIME)
timestampthis.verifies()
}
}
}
@Test
public void simpleIssuanceWithTweak() {
ledger(l -> {
.transaction(tx -> {
l.output("paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
tx.tweak(tw -> {
tx.command(getDUMMY_PUBKEY_1(), new JavaCommercialPaper.Commands.Issue());
tw.timestamp(getTEST_TX_TIME());
twreturn tw.failsWith("output states are issued by a command signer");
});
.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue());
tx.timestamp(getTEST_TX_TIME());
txreturn tx.verifies();
});
return Unit.INSTANCE;
});
}
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 transaction in this
way is very common. There is even a shorthand top-level
transaction
primitive that creates a ledger with a single
transaction:
@Test
fun `simple issuance with tweak and top level transaction`() {
{
transaction ("paper") { getPaper() } // Some CP is issued onto the ledger by MegaCorp.
output{
tweak (DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
command(TEST_TX_TIME)
timestampthis `fails with` "output states are issued by a command signer"
}
(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
command(TEST_TX_TIME)
timestampthis.verifies()
}
}
@Test
public void simpleIssuanceWithTweakTopLevelTx() {
transaction(tx -> {
.output("paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
tx.tweak(tw -> {
tx.command(getDUMMY_PUBKEY_1(), new JavaCommercialPaper.Commands.Issue());
tw.timestamp(getTEST_TX_TIME());
twreturn tw.failsWith("output states are issued by a command signer");
});
.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue());
tx.timestamp(getTEST_TX_TIME());
txreturn tx.verifies();
});
}
Chaining transactions
Now that we know how to define a single transaction, let's look at how to define a chain of them:
@Test
fun `chain commercial paper`() {
val issuer = MEGA_CORP.ref(123)
{
ledger {
unverifiedTransaction ("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY)
output}
// Some CP is issued onto the ledger by MegaCorp.
("Issuance") {
transaction("paper") { getPaper() }
output(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
command(TEST_TX_TIME)
timestampthis.verifies()
}
("Trade") {
transaction("paper")
input("alice's $900")
input("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
output("alice's paper") { "paper".output<ICommercialPaperState>() `owned by` ALICE_PUBKEY }
output(ALICE_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
commandthis.verifies()
}
}
}
@Test
public void chainCommercialPaper() {
= getMEGA_CORP().ref(defaultRef);
PartyAndReference issuer ledger(l -> {
.unverifiedTransaction(tx -> {
l.output("alice's $900",
txnew Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null));
return Unit.INSTANCE;
});
// Some CP is issued onto the ledger by MegaCorp.
.transaction("Issuance", tx -> {
l.output("paper", getPaper());
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue());
tx.timestamp(getTEST_TX_TIME());
txreturn tx.verifies();
});
.transaction("Trade", tx -> {
l.input("paper");
tx.input("alice's $900");
tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null));
tx.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
JavaCommercialPaper.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());
txreturn tx.verifies();
});
return Unit.INSTANCE;
});
}
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()
.
Notice that we labelled output with "alice's $900"
, also
in transaction named "Issuance"
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>()
.
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:
@Test
fun `chain commercial paper double spend`() {
val issuer = MEGA_CORP.ref(123)
{
ledger {
unverifiedTransaction ("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY)
output}
// Some CP is issued onto the ledger by MegaCorp.
("Issuance") {
transaction("paper") { getPaper() }
output(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
command(TEST_TX_TIME)
timestampthis.verifies()
}
("Trade") {
transaction("paper")
input("alice's $900")
input("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
output("alice's paper") { "paper".output<ICommercialPaperState>() `owned by` ALICE_PUBKEY }
output(ALICE_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
commandthis.verifies()
}
{
transaction ("paper")
input// We moved a paper to another pubkey.
("bob's paper") { "paper".output<ICommercialPaperState>() `owned by` BOB_PUBKEY }
output(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
commandthis.verifies()
}
this.fails()
}
}
@Test
public void chainCommercialPaperDoubleSpend() {
= getMEGA_CORP().ref(defaultRef);
PartyAndReference issuer ledger(l -> {
.unverifiedTransaction(tx -> {
l.output("alice's $900",
txnew Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null));
return Unit.INSTANCE;
});
// Some CP is issued onto the ledger by MegaCorp.
.transaction("Issuance", tx -> {
l.output("paper", getPaper());
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue());
tx.timestamp(getTEST_TX_TIME());
txreturn tx.verifies();
});
.transaction("Trade", tx -> {
l.input("paper");
tx.input("alice's $900");
tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null));
tx.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
JavaCommercialPaper.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());
txreturn tx.verifies();
});
.transaction(tx -> {
l.input("paper");
tx.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
JavaCommercialPaper// We moved a paper to other pubkey.
.output("bob's paper", inputPaper.withOwner(getBOB_PUBKEY()));
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
txreturn tx.verifies();
});
.fails();
lreturn Unit.INSTANCE;
});
}
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:
@Test
fun `chain commercial tweak`() {
val issuer = MEGA_CORP.ref(123)
{
ledger {
unverifiedTransaction ("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY)
output}
// Some CP is issued onto the ledger by MegaCorp.
("Issuance") {
transaction("paper") { getPaper() }
output(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
command(TEST_TX_TIME)
timestampthis.verifies()
}
("Trade") {
transaction("paper")
input("alice's $900")
input("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
output("alice's paper") { "paper".output<ICommercialPaperState>() `owned by` ALICE_PUBKEY }
output(ALICE_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
commandthis.verifies()
}
{
tweak {
transaction ("paper")
input// We moved a paper to another pubkey.
("bob's paper") { "paper".output<ICommercialPaperState>() `owned by` BOB_PUBKEY }
output(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
commandthis.verifies()
}
this.fails()
}
this.verifies()
}
}
@Test
public void chainCommercialPaperTweak() {
= getMEGA_CORP().ref(defaultRef);
PartyAndReference issuer ledger(l -> {
.unverifiedTransaction(tx -> {
l.output("alice's $900",
txnew Cash.State(issuedBy(DOLLARS(900), issuer), getALICE_PUBKEY(), null));
return Unit.INSTANCE;
});
// Some CP is issued onto the ledger by MegaCorp.
.transaction("Issuance", tx -> {
l.output("paper", getPaper());
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Issue());
tx.timestamp(getTEST_TX_TIME());
txreturn tx.verifies();
});
.transaction("Trade", tx -> {
l.input("paper");
tx.input("alice's $900");
tx.output("borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), getMEGA_CORP_PUBKEY(), null));
tx.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
JavaCommercialPaper.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());
txreturn tx.verifies();
});
.tweak(lw -> {
l.transaction(tx -> {
lw.input("paper");
tx.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
JavaCommercialPaper// We moved a paper to another pubkey.
.output("bob's paper", inputPaper.withOwner(getBOB_PUBKEY()));
tx.command(getMEGA_CORP_PUBKEY(), new JavaCommercialPaper.Commands.Move());
txreturn tx.verifies();
});
.fails();
lwreturn Unit.INSTANCE;
});
.verifies();
lreturn Unit.INSTANCE;
});
}