From 163175860ddf32853fef14943fb9c3aa9591e1d0 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 6 Nov 2015 16:35:51 +0100 Subject: [PATCH] Improve the contracts DSL, make the Cash contract extend the DSL in a small way to make working with arrays of cash outputs easier. --- src/contracts/Cash.kt | 22 +++++++++++----------- src/core/ContractsDSL.kt | 13 ++++++++++++- src/core/Crypto.kt | 7 +++++++ src/core/Structures.kt | 12 ++++++------ src/core/TestUtils.kt | 4 ++-- tests/contracts/CashTests.kt | 11 ++++++----- 6 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/contracts/Cash.kt b/src/contracts/Cash.kt index e2d74969cd..2a7b13e227 100644 --- a/src/contracts/Cash.kt +++ b/src/contracts/Cash.kt @@ -1,3 +1,5 @@ +package contracts + import core.* import java.security.PublicKey @@ -11,7 +13,7 @@ val CASH_PROGRAM_ID = SecureHash.sha256("cash") /** A state representing a claim on the cash reserves of some institution */ data class CashState( /** Where the underlying currency backing this ledger entry can be found (propagated) */ - val deposit: DepositPointer, + val deposit: InstitutionReference, val amount: Amount, @@ -35,6 +37,9 @@ class ExitCashCommand(val amount: Amount) : Command { class InsufficientBalanceException(val amountMissing: Amount) : Exception() +// Small DSL extension. +fun Iterable.sumCashBy(owner: PublicKey) = this.filterIsInstance().filter { it.owner == owner }.map { it.amount }.sum() + /** * A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple * input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour @@ -50,7 +55,7 @@ class InsufficientBalanceException(val amountMissing: Amount) : Exception() */ object CashContract : Contract { /** This is the function EVERYONE runs */ - override fun verify(inStates: List, outStates: List, args: List) { + override fun verify(inStates: List, outStates: List, args: List>) { val cashInputs = inStates.filterIsInstance() requireThat { @@ -77,13 +82,8 @@ object CashContract : Contract { val inputAmount = inputs.map { it.amount }.sum() val outputAmount = outputs.map { it.amount }.sumOrZero(currency) - val issuerCommand = args. - filter { it.signingInstitution == deposit.institution }. - // TODO: this map+filterNotNull pattern will become a single function in the next Kotlin beta. - map { it.command as? ExitCashCommand }. - filterNotNull(). - singleOrNull() - val amountExitingLedger = issuerCommand?.amount ?: Amount(0, inputAmount.currency) + val issuerCommand = args.select(institution = deposit.institution).singleOrNull() + val amountExitingLedger = issuerCommand?.value?.amount ?: Amount(0, inputAmount.currency) requireThat { "for deposit ${deposit.reference} at issuer ${deposit.institution.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger) @@ -96,7 +96,7 @@ object CashContract : Contract { // see a signature from each of those keys. The actual signatures have been verified against the transaction // data by the platform before execution. val owningPubKeys = cashInputs.map { it.owner }.toSortedSet() - val keysThatSigned = args.filter { it.command is MoveCashCommand }.map { it.signer }.toSortedSet() + val keysThatSigned = args.select().map { it.signer }.toSortedSet() requireThat { "the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned) } @@ -167,7 +167,7 @@ object CashContract : Contract { } else states // Finally, generate the commands. Pretend to sign here, real signatures aren't done yet. - val commands = keysUsed.map { VerifiedSignedCommand(it, null, MoveCashCommand()) } + val commands = keysUsed.map { VerifiedSigned(it, null, MoveCashCommand()) } return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList()) } diff --git a/src/core/ContractsDSL.kt b/src/core/ContractsDSL.kt index 7a0ea2f1bf..10933de786 100644 --- a/src/core/ContractsDSL.kt +++ b/src/core/ContractsDSL.kt @@ -1,6 +1,7 @@ package core import java.math.BigDecimal +import java.security.PublicKey import java.util.* import kotlin.math.div @@ -14,6 +15,17 @@ import kotlin.math.div // TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354) +// region Misc +inline fun List>.select(signer: PublicKey? = null, institution: Institution? = null) = + filter { it.value is T }. + filter { if (signer == null) true else signer == it.signer }. + filter { if (institution == null) true else institution == it.signingInstitution }. + map { VerifiedSigned(it.signer, it.signingInstitution, it.value as T) } + +inline fun List>.requireSingleCommand() = select().single() + +// endregion + // region Currencies /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// fun currency(code: String) = Currency.getInstance(code) @@ -46,7 +58,6 @@ fun requireThat(body: Requirements.() -> Unit) { // endregion - // region Amounts /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/core/Crypto.kt b/src/core/Crypto.kt index 779fb1a67f..755452e093 100644 --- a/src/core/Crypto.kt +++ b/src/core/Crypto.kt @@ -36,3 +36,10 @@ sealed class DigitalSignature(bits: ByteArray, val covering: Int) : OpaqueBytes( class LegallyIdentifiable(val signer: Institution, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering) } +object NullPublicKey : PublicKey, Comparable { + override fun getAlgorithm() = "NULL" + override fun getEncoded() = byteArrayOf(0) + override fun getFormat() = "NULL" + override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1 + override fun toString() = "NULL_KEY" +} diff --git a/src/core/Structures.kt b/src/core/Structures.kt index 069af40751..ace02471e8 100644 --- a/src/core/Structures.kt +++ b/src/core/Structures.kt @@ -76,11 +76,11 @@ data class SignedCommand( ) /** Obtained from a [SignedCommand], deserialised and signature checked */ -data class VerifiedSignedCommand( +data class VerifiedSigned( val signer: PublicKey, /** If the public key was recognised, the looked up institution is available here, otherwise it's null */ val signingInstitution: Institution?, - val command: Command + val value: T ) /** @@ -90,13 +90,13 @@ data class VerifiedSignedCommand( */ interface Contract { /** Must throw an exception if there's a problem that should prevent state transition. */ - fun verify(inStates: List, outStates: List, args: List) + fun verify(inStates: List, outStates: List, args: List>) } /** - * Reference to some money being stored by an institution e.g. in a vault or (more likely) on their normal ledger. - * The deposit reference is intended to be encrypted so it's meaningless to anyone other than the institution. + * Reference to something being stored or issued by an institution e.g. in a vault or (more likely) on their normal + * ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the institution. */ -data class DepositPointer(val institution: Institution, val reference: OpaqueBytes) { +data class InstitutionReference(val institution: Institution, val reference: OpaqueBytes) { override fun toString() = "${institution.name}$reference" } \ No newline at end of file diff --git a/src/core/TestUtils.kt b/src/core/TestUtils.kt index 28fb15aef7..9ef53aeee1 100644 --- a/src/core/TestUtils.kt +++ b/src/core/TestUtils.kt @@ -50,11 +50,11 @@ val TEST_KEYS_TO_CORP_MAP: Map = mapOf( data class TransactionForTest( private val inStates: MutableList = arrayListOf(), private val outStates: MutableList = arrayListOf(), - private val args: MutableList = arrayListOf() + private val args: MutableList> = arrayListOf() ) { fun input(s: () -> ContractState) = inStates.add(s()) fun output(s: () -> ContractState) = outStates.add(s()) - fun arg(key: PublicKey, c: () -> Command) = args.add(VerifiedSignedCommand(key, TEST_KEYS_TO_CORP_MAP[key], c())) + fun arg(key: PublicKey, c: () -> Command) = args.add(VerifiedSigned(key, TEST_KEYS_TO_CORP_MAP[key], c())) infix fun Contract.`fails requirement`(msg: String) { try { diff --git a/tests/contracts/CashTests.kt b/tests/contracts/CashTests.kt index e31933bedc..da4bae5c30 100644 --- a/tests/contracts/CashTests.kt +++ b/tests/contracts/CashTests.kt @@ -1,3 +1,4 @@ +import contracts.* import core.* import org.junit.Test import kotlin.test.assertEquals @@ -9,7 +10,7 @@ import kotlin.test.assertFailsWith class CashTests { val inState = CashState( - deposit = DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), + deposit = InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), amount = 1000.DOLLARS, owner = DUMMY_PUBKEY_1 ) @@ -222,10 +223,10 @@ class CashTests { val OUR_PUBKEY_1 = DUMMY_PUBKEY_1 val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2 val WALLET = listOf( - CashState(DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1), - CashState(DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1), - CashState(DepositPointer(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1), - CashState(DepositPointer(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1) + CashState(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1), + CashState(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1), + CashState(InstitutionReference(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1), + CashState(InstitutionReference(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1) ) @Test