From 0131c2cc1d007780ab778b74f9dc7a39d3ca743d Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 6 Nov 2015 17:55:20 +0100 Subject: [PATCH] CP: check in a "childrens paper" contract that is sort of like commercial paper, but not really. Tests are incomplete. --- src/contracts/ChildrensPaper.kt | 65 ++++++++++++++++++++++++++ src/core/TestUtils.kt | 23 +++++++-- src/core/Utils.kt | 8 +++- tests/contracts/ChildrensPaperTests.kt | 39 ++++++++++++++++ 4 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 src/contracts/ChildrensPaper.kt create mode 100644 tests/contracts/ChildrensPaperTests.kt diff --git a/src/contracts/ChildrensPaper.kt b/src/contracts/ChildrensPaper.kt new file mode 100644 index 0000000000..470772a8e0 --- /dev/null +++ b/src/contracts/ChildrensPaper.kt @@ -0,0 +1,65 @@ +package contracts + +import core.* +import java.security.PublicKey +import java.time.Instant + +/** + * "Children's paper" is basically like commercial paper, but modelled by a non-expert and without any attention paid + * to the issuance aspect. It has a weird name to emphasise that it's not a prototype of real CP, as that's currently + * waiting for Jo/Ayoub to deliver full CP use case models. This file may be renamed later if it grows up and + * becomes real CP. + * + * Open issues: + * - In this model, you cannot merge or split CP. Can you do this normally? We could model CP as a specialised form + * of cash, or reuse some of the cash code? + * - Currently cannot trade more than one piece of CP in a single transaction. This is probably going to be a common + * issue: need to find a cleaner way to allow this. Does the single-execution-per-transaction model make sense? + */ + +val CP_PROGRAM_ID = SecureHash.sha256("childrens-paper") + +data class ChildrensPaperState( + val issuance: InstitutionReference, + val owner: PublicKey, + val faceValue: Amount, + val maturityDate: Instant +) : ContractState { + override val programRef = CP_PROGRAM_ID + + fun withoutOwner() = copy(owner = NullPublicKey) +} + +// TODO: Generalise the notion of an owned object into a superclass/supercontract. Consider composition vs inheritance. +sealed class CPCommands : Command { + class MoveCommand : CPCommands() + class RedeemCommand : CPCommands() +} + +object ChildrensPaper : Contract { + override fun verify(inStates: List, outStates: List, args: List>, time: Instant) { + // There are two possible things that can be done with CP. The first is trading it. The second is redeeming it + // for cash on or after the maturity date. + val command = args.requireSingleCommand() + + // For now do not allow multiple pieces of CP to trade in a single transaction. Study this more! + val input = inStates.filterIsInstance().single() + val output = outStates.filterIsInstance().single() + + when (command.value) { + is CPCommands.MoveCommand -> requireThat { + "the transaction is signed by the owner of the CP" by (command.signer == input.owner) + "the output state is the same as the input state except for owner" by (input.withoutOwner() == output.withoutOwner()) + } + + is CPCommands.RedeemCommand -> { + val received = outStates.sumCashBy(command.signer) + requireThat { + "the paper must have matured" by (input.maturityDate < time) + "the received amount equals the face value" by (received == input.faceValue) + } + } + } + } +} + diff --git a/src/core/TestUtils.kt b/src/core/TestUtils.kt index 0813fb289a..cbc9ed0f83 100644 --- a/src/core/TestUtils.kt +++ b/src/core/TestUtils.kt @@ -26,6 +26,9 @@ val TEST_KEYS_TO_CORP_MAP: Map = mapOf( MINI_CORP_KEY to MINI_CORP ) +// A dummy time at which we will be pretending test transactions are created. +val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z") + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes. @@ -59,21 +62,33 @@ data class TransactionForTest( infix fun Contract.`fails requirement`(msg: String) { try { - verify(inStates, outStates, args, Instant.now()) + verify(inStates, outStates, args, TEST_TX_TIME) } catch(e: Exception) { val m = e.message if (m == null) fail("Threw exception without a message") else - if (!m.contains(msg)) throw AssertionError("Error was actually: $m") + if (!m.contains(msg)) throw AssertionError("Error was actually: $m", e) } } // which is uglier?? :) fun Contract.fails_requirement(msg: String) = this.`fails requirement`(msg) - fun Contract.accepts() { - verify(inStates, outStates, args, Instant.now()) + fun Contract.accepts() = verify(inStates, outStates, args, TEST_TX_TIME) + fun Contract.rejects(withMessage: String? = null) { + val r = try { + accepts() + false + } catch (e: Exception) { + val m = e.message + if (m == null) + fail("Threw exception without a message") + else + if (withMessage != null && !m.contains(withMessage)) throw AssertionError("Error was actually: $m", e) + true + } + if (!r) throw AssertionError("Expected exception but didn't get one") } // Allow customisation of partial transactions. diff --git a/src/core/Utils.kt b/src/core/Utils.kt index f3fdc47f96..edde942f23 100644 --- a/src/core/Utils.kt +++ b/src/core/Utils.kt @@ -1,6 +1,7 @@ package core import com.google.common.io.BaseEncoding +import java.time.Duration import java.util.* /** A simple class that wraps a byte array and makes the equals/hashCode/toString methods work as you actually expect */ @@ -18,4 +19,9 @@ open class OpaqueBytes(val bits: ByteArray) { override fun hashCode() = Arrays.hashCode(bits) override fun toString() = "[" + BaseEncoding.base16().encode(bits) + "]" -} \ No newline at end of file +} + +val Int.days: Duration get() = Duration.ofDays(this.toLong()) +val Int.hours: Duration get() = Duration.ofHours(this.toLong()) +val Int.minutes: Duration get() = Duration.ofMinutes(this.toLong()) +val Int.seconds: Duration get() = Duration.ofSeconds(this.toLong()) \ No newline at end of file diff --git a/tests/contracts/ChildrensPaperTests.kt b/tests/contracts/ChildrensPaperTests.kt new file mode 100644 index 0000000000..37c7c888c5 --- /dev/null +++ b/tests/contracts/ChildrensPaperTests.kt @@ -0,0 +1,39 @@ +package contracts + +import core.* +import org.junit.Test + +// TODO: Finish this off. + +class ChildrensPaperTests { + val contract = ChildrensPaper + + val PAPER_1 = ChildrensPaperState( + issuance = InstitutionReference(MEGA_CORP, OpaqueBytes.of(123)), + owner = DUMMY_PUBKEY_1, + faceValue = 1000.DOLLARS, + maturityDate = TEST_TX_TIME + 7.days + ) + val PAPER_2 = PAPER_1.copy(owner = DUMMY_PUBKEY_2) + + @Test + fun move() { + // One entity sells the paper to another (e.g. the issuer sells it to a first time buyer) + transaction { + input { PAPER_1 } + output { PAPER_2 } + + contract.rejects() + + transaction { + arg(DUMMY_PUBKEY_2) { CPCommands.MoveCommand() } + contract `fails requirement` "is signed by the owner" + } + + transaction { + arg(DUMMY_PUBKEY_1) { CPCommands.MoveCommand() } + contract.accepts() + } + } + } +} \ No newline at end of file