20 KiB
Writing a contract test
This tutorial will take you through the steps required to write a contract test using Kotlin and/or 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:
@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.
Let's add a Cash transaction:
@Test
fun simpleCashDoesntCompile() {
val inState = Cash.State(
= 1000.DOLLARS `issued by` DUMMY_CASH_ISSUER,
amount = DUMMY_PUBKEY_1
owner )
{
ledger {
transaction (inState)
input}
}
}
@Test
public void simpleCashDoesntCompile() {
.State inState = new Cash.State(
CashissuedBy(DOLLARS(1000), getDUMMY_CASH_ISSUER()),
getDUMMY_PUBKEY_1()
);
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:
:(26, 21) Kotlin: Type mismatch: inferred type is Unit but EnforceVerifyOrFail was expected Error
Error:(26, 31) 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 simpleCash() {
val inState = Cash.State(
= 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
amount = DUMMY_PUBKEY_1
owner )
{
ledger {
transaction (inState)
inputthis.verifies()
}
}
}
@Test
public void simpleCash() {
.State inState = new Cash.State(
CashissuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
getDUMMY_PUBKEY_1()
);
ledger(l -> {
.transaction(tx -> {
l.input(inState);
txreturn tx.verifies();
});
return Unit.INSTANCE;
});
}
The code finally compiles. When run, it produces the following error:
.corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalArgumentException: Failed requirement: for deposit [01] at issuer Snake Oil Issuer the amounts balance net
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"
:
@Test
fun simpleCashFailsWith() {
val inState = Cash.State(
= 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
amount = DUMMY_PUBKEY_1
owner )
{
ledger {
transaction (inState)
inputthis `fails with` "the amounts balance"
}
}
}
@Test
public void simpleCashFailsWith() {
.State inState = new Cash.State(
CashissuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
getDUMMY_PUBKEY_1()
);
ledger(l -> {
.transaction(tx -> {
l.input(inState);
txreturn tx.failsWith("the amounts balance");
});
return Unit.INSTANCE;
});
}
We can continue to build the transaction until it
verifies
:
@Test
fun simpleCashSuccess() {
val inState = Cash.State(
= 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
amount = DUMMY_PUBKEY_1
owner )
{
ledger {
transaction (inState)
inputthis `fails with` "the amounts balance"
(inState.copy(owner = DUMMY_PUBKEY_2))
output(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
commandthis.verifies()
}
}
}
@Test
public void simpleCashSuccess() {
.State inState = new Cash.State(
CashissuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
getDUMMY_PUBKEY_1()
);
ledger(l -> {
.transaction(tx -> {
l.input(inState);
tx.failsWith("the amounts balance");
tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2()));
tx.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
txreturn tx.verifies();
});
return Unit.INSTANCE;
});
}
output
specifies that we want the input state to be
transferred to DUMMY_PUBKEY_2
and command
adds
the Move
command itself, signed by the current owner of the
input state, DUMMY_PUBKEY_1
.
We constructed a complete signed cash transaction from
DUMMY_PUBKEY_1
to DUMMY_PUBKEY_2
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 ruin the transaction for good... Enter tweak
:
@Test
fun simpleCashTweakSuccess() {
val inState = Cash.State(
= 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
amount = DUMMY_PUBKEY_1
owner )
{
ledger {
transaction (inState)
inputthis `fails with` "the amounts balance"
(inState.copy(owner = DUMMY_PUBKEY_2))
output
{
tweak (DUMMY_PUBKEY_2) { Cash.Commands.Move() }
commandthis `fails with` "the owning keys are the same as the signing keys"
}
(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
commandthis.verifies()
}
}
}
@Test
public void simpleCashTweakSuccess() {
.State inState = new Cash.State(
CashissuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
getDUMMY_PUBKEY_1()
);
ledger(l -> {
.transaction(tx -> {
l.input(inState);
tx.failsWith("the amounts balance");
tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2()));
tx
.tweak(tw -> {
tx.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move());
twreturn tw.failsWith("the owning keys are the same as the signing keys");
});
.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
txreturn tx.verifies();
});
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.
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 ledger with a single
transaction:
@Test
fun simpleCashTweakSuccessTopLevelTransaction() {
val inState = Cash.State(
= 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
amount = DUMMY_PUBKEY_1
owner )
{
transaction (inState)
inputthis `fails with` "the amounts balance"
(inState.copy(owner = DUMMY_PUBKEY_2))
output
{
tweak (DUMMY_PUBKEY_2) { Cash.Commands.Move() }
commandthis `fails with` "the owning keys are the same as the signing keys"
}
(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
commandthis.verifies()
}
}
@Test
public void simpleCashTweakSuccessTopLevelTransaction() {
.State inState = new Cash.State(
CashissuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
getDUMMY_PUBKEY_1()
);
transaction(tx -> {
.input(inState);
tx.failsWith("the amounts balance");
tx.output(inState.copy(inState.getAmount(), getDUMMY_PUBKEY_2()));
tx
.tweak(tw -> {
tx.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move());
twreturn tw.failsWith("the owning keys are the same as the signing keys");
});
.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
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 chainCash() {
{
ledger {
unverifiedTransaction ("MEGA_CORP cash") {
output.State(
Cash= 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
amount = MEGA_CORP_PUBKEY
owner )
}
}
{
transaction ("MEGA_CORP cash")
input("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_1))
output(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
commandthis.verifies()
}
}
}
@Test
public void chainCash() {
ledger(l -> {
.unverifiedTransaction(tx -> {
l.output("MEGA_CORP cash",
txnew Cash.State(
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
getMEGA_CORP_PUBKEY()
)
);
return Unit.INSTANCE;
});
.transaction(tx -> {
l.input("MEGA_CORP cash");
tx.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash");
Cash.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1()));
tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
txreturn tx.verifies();
});
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
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. by input("MEGA_CORP cash")
or
"MEGA_CORP cash".output<Cash.State>()
.
What happens if we reuse the output cash twice?
@Test
fun chainCashDoubleSpend() {
{
ledger {
unverifiedTransaction ("MEGA_CORP cash") {
output.State(
Cash= 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
amount = MEGA_CORP_PUBKEY
owner )
}
}
{
transaction ("MEGA_CORP cash")
input("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_1))
output(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
commandthis.verifies()
}
{
transaction ("MEGA_CORP cash")
input// We send it to another pubkey so that the transaction is not identical to the previous one
("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_2))
output(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
commandthis.verifies()
}
}
}
@Test
public void chainCashDoubleSpend() {
ledger(l -> {
.unverifiedTransaction(tx -> {
l.output("MEGA_CORP cash",
txnew Cash.State(
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
getMEGA_CORP_PUBKEY()
)
);
return Unit.INSTANCE;
});
.transaction(tx -> {
l.input("MEGA_CORP cash");
tx.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash");
Cash.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1()));
tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
txreturn tx.verifies();
});
.transaction(tx -> {
l.input("MEGA_CORP cash");
tx.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash");
Cash// We send it to another pubkey so that the transaction is not identical to the previous one
.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_2()));
tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
txreturn tx.verifies();
});
return Unit.INSTANCE;
});
}
The transactions verifies()
individually, however the
state was spent twice!
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:
@Test
fun chainCashDoubleSpendFailsWith() {
{
ledger {
unverifiedTransaction ("MEGA_CORP cash") {
output.State(
Cash= 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
amount = MEGA_CORP_PUBKEY
owner )
}
}
{
transaction ("MEGA_CORP cash")
input("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_1))
output(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
commandthis.verifies()
}
{
tweak {
transaction ("MEGA_CORP cash")
input// We send it to another pubkey so that the transaction is not identical to the previous one
("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_1))
output(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
commandthis.verifies()
}
this.fails()
}
this.verifies()
}
}
@Test
public void chainCashDoubleSpendFailsWith() {
ledger(l -> {
.unverifiedTransaction(tx -> {
l.output("MEGA_CORP cash",
txnew Cash.State(
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)),
getMEGA_CORP_PUBKEY()
)
);
return Unit.INSTANCE;
});
.transaction(tx -> {
l.input("MEGA_CORP cash");
tx.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash");
Cash.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_1()));
tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
txreturn tx.verifies();
});
.tweak(lw -> {
l.transaction(tx -> {
lw.input("MEGA_CORP cash");
tx.State inputCash = l.retrieveOutput(Cash.State.class, "MEGA_CORP cash");
Cash// We send it to another pubkey so that the transaction is not identical to the previous one
.output(inputCash.copy(inputCash.getAmount(), getDUMMY_PUBKEY_2()));
tx.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
txreturn tx.verifies();
});
.fails();
lwreturn Unit.INSTANCE;
});
.verifies();
lreturn Unit.INSTANCE;
});
}