From 1628c1e17ac8c8106582b373a4cbb8d79ffa86a2 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 27 Nov 2015 15:44:43 +0100 Subject: [PATCH] Implement and test crafting/generate methods for CommercialPaper. Rename test keys and use real EC keys instead of dummies. --- src/contracts/CommercialPaper.kt | 34 ++++++++++++ tests/contracts/CashTests.kt | 14 ++--- tests/contracts/CommercialPaperTests.kt | 70 ++++++++++++++++++++----- tests/core/TransactionGroupTests.kt | 16 +++--- tests/core/testutils/TestUtils.kt | 20 ++++--- 5 files changed, 118 insertions(+), 36 deletions(-) diff --git a/src/contracts/CommercialPaper.kt b/src/contracts/CommercialPaper.kt index 001d727f74..fbc76c76b4 100644 --- a/src/contracts/CommercialPaper.kt +++ b/src/contracts/CommercialPaper.kt @@ -94,5 +94,39 @@ class CommercialPaper : Contract { } } } + + /** + * Returns a transaction that issues commercial paper, owned by the issuing institution's key. Does not update + * an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction + * at the moment: this restriction is not fundamental and may be lifted later. + */ + fun craftIssue(issuance: InstitutionReference, faceValue: Amount, maturityDate: Instant): PartialTransaction { + val state = State(issuance, issuance.institution.owningKey, faceValue, maturityDate) + return PartialTransaction(state, WireCommand(Commands.Issue, issuance.institution.owningKey)) + } + + /** + * Updates the given partial transaction with an input/output/command to reassign ownership of the paper. + */ + fun craftMove(tx: PartialTransaction, paper: StateAndRef, newOwner: PublicKey) { + tx.addInputState(paper.ref) + tx.addOutputState(paper.state.copy(owner = newOwner)) + tx.addArg(WireCommand(Commands.Move, paper.state.owner)) + } + + /** + * Intended to be called by the issuer of some commercial paper, when an owner has notified us that they wish + * to redeem the paper. We must therefore send enough money to the key that owns the paper to satisfy the face + * value, and then ensure the paper is removed from the ledger. + * + * @throws InsufficientBalanceException if the wallet doesn't contain enough money to pay the redeemer + */ + @Throws(InsufficientBalanceException::class) + fun craftRedeem(tx: PartialTransaction, paper: StateAndRef, wallet: List>) { + // Add the cash movement using the states in our wallet. + Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet) + tx.addInputState(paper.ref) + tx.addArg(WireCommand(CommercialPaper.Commands.Redeem, paper.state.owner)) + } } diff --git a/tests/contracts/CashTests.kt b/tests/contracts/CashTests.kt index 10a79bdba4..e5130ea3fa 100644 --- a/tests/contracts/CashTests.kt +++ b/tests/contracts/CashTests.kt @@ -83,10 +83,10 @@ class CashTests { ) } tweak { - arg(MINI_CORP_KEY) { Cash.Commands.Issue(0) } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) } this `fails requirement` "has a nonce" } - arg(MINI_CORP_KEY) { Cash.Commands.Issue() } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() } this.accepts() } @@ -98,7 +98,7 @@ class CashTests { assertEquals(MINI_CORP, s.deposit.institution) assertEquals(DUMMY_PUBKEY_1, s.owner) assertTrue(ptx.commands()[0].command is Cash.Commands.Issue) - assertEquals(MINI_CORP_KEY, ptx.commands()[0].pubkeys[0]) + assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].pubkeys[0]) } @Test @@ -196,13 +196,13 @@ class CashTests { output { outState.copy(amount = inState.amount - 200.DOLLARS) } tweak { - arg(MEGA_CORP_KEY) { Cash.Commands.Exit(100.DOLLARS) } + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS) } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move } this `fails requirement` "the amounts balance" } tweak { - arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) } + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) } this `fails requirement` "required contracts.Cash.Commands.Move command" tweak { @@ -223,10 +223,10 @@ class CashTests { this `fails requirement` "at issuer MegaCorp the amounts balance" - arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) } + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) } this `fails requirement` "at issuer MiniCorp the amounts balance" - arg(MINI_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) } this.accepts() } } diff --git a/tests/contracts/CommercialPaperTests.kt b/tests/contracts/CommercialPaperTests.kt index 1bb3842152..0f3f8bf313 100644 --- a/tests/contracts/CommercialPaperTests.kt +++ b/tests/contracts/CommercialPaperTests.kt @@ -1,16 +1,16 @@ package contracts -import core.Amount -import core.DOLLARS -import core.days +import core.* import core.testutils.* import org.junit.Test import java.time.Instant +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue class CommercialPaperTests { val PAPER_1 = CommercialPaper.State( issuance = MEGA_CORP.ref(123), - owner = MEGA_CORP_KEY, + owner = MEGA_CORP_PUBKEY, faceValue = 1000.DOLLARS, maturityDate = TEST_TX_TIME + 7.days ) @@ -42,7 +42,7 @@ class CommercialPaperTests { transactionGroup { transaction { output { PAPER_1.copy(faceValue = 0.DOLLARS) } - arg(MEGA_CORP_KEY) { CommercialPaper.Commands.Issue } + arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue } } expectFailureOfTx(1, "face value is not zero") @@ -54,7 +54,7 @@ class CommercialPaperTests { transactionGroup { transaction { output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) } - arg(MEGA_CORP_KEY) { CommercialPaper.Commands.Issue } + arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue } } expectFailureOfTx(1, "maturity date is not in the past") @@ -70,7 +70,7 @@ class CommercialPaperTests { transaction { input("paper") output { PAPER_1 } - arg(MEGA_CORP_KEY) { CommercialPaper.Commands.Issue } + arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue } } expectFailureOfTx(1, "there is no input state") @@ -95,13 +95,13 @@ class CommercialPaperTests { return transactionGroupFor() { roots { transaction(900.DOLLARS.CASH `owned by` ALICE label "alice's $900") - transaction(someProfits.CASH `owned by` MEGA_CORP_KEY label "some profits") + transaction(someProfits.CASH `owned by` MEGA_CORP_PUBKEY label "some profits") } // Some CP is issued onto the ledger by MegaCorp. transaction { output("paper") { PAPER_1 } - arg(MEGA_CORP_KEY) { CommercialPaper.Commands.Issue } + arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue } } // The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days, @@ -109,10 +109,10 @@ class CommercialPaperTests { transaction { input("paper") input("alice's $900") - output { 900.DOLLARS.CASH `owned by` MEGA_CORP_KEY } + output { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY } output("alice's paper") { "paper".output `owned by` ALICE } arg(ALICE) { Cash.Commands.Move } - arg(MEGA_CORP_KEY) { CommercialPaper.Commands.Move } + arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move } } // Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200 @@ -122,13 +122,57 @@ class CommercialPaperTests { input("some profits") output { aliceGetsBack.CASH `owned by` ALICE } - output { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_KEY } + output { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_PUBKEY } if (!destroyPaperAtRedemption) output { "paper".output } - arg(MEGA_CORP_KEY) { Cash.Commands.Move } + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move } arg(ALICE) { CommercialPaper.Commands.Redeem } } } } + + @Test + fun `issue move and then redeem`() { + val issueTX: LedgerTransaction = run { + val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days) + ptx.signWith(MINI_CORP_KEY) + val stx = ptx.toSignedTransaction() + stx.verify().toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256()) + } + + val moveTX: LedgerTransaction = run { + val ptx = PartialTransaction() + CommercialPaper().craftMove(ptx, issueTX.outRef(0), ALICE) + ptx.signWith(MINI_CORP_KEY) + val stx = ptx.toSignedTransaction() + stx.verify().toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256()) + } + + // Won't be validated. + val someCash = LedgerTransaction(emptyList(), listOf( + 9000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY, + 4000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY + ), emptyList(), TEST_TX_TIME, SecureHash.randomSHA256()) + val wallet = listOf>(someCash.outRef(0), someCash.outRef(1)) + + + fun makeRedeemTX(time: Instant): LedgerTransaction { + val ptx = PartialTransaction() + CommercialPaper().craftRedeem(ptx, moveTX.outRef(0), wallet) + ptx.signWith(ALICE_KEY) + ptx.signWith(MINI_CORP_KEY) + return ptx.toSignedTransaction().verify().toLedgerTransaction(time, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256()) + } + + val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days) + val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days) + + val e = assertFailsWith(TransactionVerificationException::class) { + TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(someCash)).verify(TEST_PROGRAM_MAP) + } + assertTrue(e.cause!!.message!!.contains("paper must have matured")) + + TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(someCash)).verify(TEST_PROGRAM_MAP) + } } \ No newline at end of file diff --git a/tests/core/TransactionGroupTests.kt b/tests/core/TransactionGroupTests.kt index f384a72b33..dc5a566643 100644 --- a/tests/core/TransactionGroupTests.kt +++ b/tests/core/TransactionGroupTests.kt @@ -8,7 +8,7 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals class TransactionGroupTests { - val A_THOUSAND_POUNDS = Cash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_KEY) + val A_THOUSAND_POUNDS = Cash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY) @Test fun success() { @@ -20,13 +20,13 @@ class TransactionGroupTests { transaction { input("£1000") output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE } - arg(MINI_CORP_KEY) { Cash.Commands.Move } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Move } } transaction { input("alice's £1000") arg(ALICE) { Cash.Commands.Move } - arg(MINI_CORP_KEY) { Cash.Commands.Exit(1000.POUNDS) } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) } } verify() @@ -38,7 +38,7 @@ class TransactionGroupTests { transactionGroup { val t = transaction { output("cash") { A_THOUSAND_POUNDS } - arg(MINI_CORP_KEY) { Cash.Commands.Issue() } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() } } val conflict1 = transaction { @@ -46,7 +46,7 @@ class TransactionGroupTests { val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB output { HALF } output { HALF } - arg(MINI_CORP_KEY) { Cash.Commands.Move } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Move } } verify() @@ -57,7 +57,7 @@ class TransactionGroupTests { val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE output { HALF } output { HALF } - arg(MINI_CORP_KEY) { Cash.Commands.Move } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Move } } assertNotEquals(conflict1, conflict2) @@ -76,7 +76,7 @@ class TransactionGroupTests { val tg = transactionGroup { transaction { output("cash") { A_THOUSAND_POUNDS } - arg(MINI_CORP_KEY) { Cash.Commands.Issue() } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() } } transaction { @@ -110,7 +110,7 @@ class TransactionGroupTests { input("£1000") input("£1000") output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) } - arg(MINI_CORP_KEY) { Cash.Commands.Move } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Move } } assertFailsWith(TransactionConflictException::class) { diff --git a/tests/core/testutils/TestUtils.kt b/tests/core/testutils/TestUtils.kt index ceda6a24d5..faed264157 100644 --- a/tests/core/testutils/TestUtils.kt +++ b/tests/core/testutils/TestUtils.kt @@ -18,18 +18,22 @@ object TestUtils { } // A few dummy values for testing. -val MEGA_CORP_KEY = TestUtils.keypair.public -val MINI_CORP_KEY = TestUtils.keypair2.public +val MEGA_CORP_KEY = TestUtils.keypair +val MEGA_CORP_PUBKEY = MEGA_CORP_KEY.public +val MINI_CORP_KEY = TestUtils.keypair2 +val MINI_CORP_PUBKEY = MINI_CORP_KEY.public val DUMMY_PUBKEY_1 = DummyPublicKey("x1") val DUMMY_PUBKEY_2 = DummyPublicKey("x2") -val ALICE = DummyPublicKey("alice") -val BOB = DummyPublicKey("bob") -val MEGA_CORP = Institution("MegaCorp", MEGA_CORP_KEY) -val MINI_CORP = Institution("MiniCorp", MINI_CORP_KEY) +val ALICE_KEY = KeyPairGenerator.getInstance("EC").genKeyPair() +val ALICE = ALICE_KEY.public +val BOB_KEY = KeyPairGenerator.getInstance("EC").genKeyPair() +val BOB = BOB_KEY.public +val MEGA_CORP = Institution("MegaCorp", MEGA_CORP_PUBKEY) +val MINI_CORP = Institution("MiniCorp", MINI_CORP_PUBKEY) val TEST_KEYS_TO_CORP_MAP: Map = mapOf( - MEGA_CORP_KEY to MEGA_CORP, - MINI_CORP_KEY to MINI_CORP + MEGA_CORP_PUBKEY to MEGA_CORP, + MINI_CORP_PUBKEY to MINI_CORP ) // A dummy time at which we will be pretending test transactions are created.