Improve the contracts DSL, make the Cash contract extend the DSL in a small way to make working with arrays of cash outputs easier.

This commit is contained in:
Mike Hearn 2015-11-06 16:35:51 +01:00
parent f09c624c0f
commit 163175860d
6 changed files with 44 additions and 25 deletions

View File

@ -1,3 +1,5 @@
package contracts
import core.* import core.*
import java.security.PublicKey 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 */ /** A state representing a claim on the cash reserves of some institution */
data class CashState( data class CashState(
/** Where the underlying currency backing this ledger entry can be found (propagated) */ /** Where the underlying currency backing this ledger entry can be found (propagated) */
val deposit: DepositPointer, val deposit: InstitutionReference,
val amount: Amount, val amount: Amount,
@ -35,6 +37,9 @@ class ExitCashCommand(val amount: Amount) : Command {
class InsufficientBalanceException(val amountMissing: Amount) : Exception() class InsufficientBalanceException(val amountMissing: Amount) : Exception()
// Small DSL extension.
fun Iterable<ContractState>.sumCashBy(owner: PublicKey) = this.filterIsInstance<CashState>().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 * 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 * 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 { object CashContract : Contract {
/** This is the function EVERYONE runs */ /** This is the function EVERYONE runs */
override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSignedCommand>) { override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>) {
val cashInputs = inStates.filterIsInstance<CashState>() val cashInputs = inStates.filterIsInstance<CashState>()
requireThat { requireThat {
@ -77,13 +82,8 @@ object CashContract : Contract {
val inputAmount = inputs.map { it.amount }.sum() val inputAmount = inputs.map { it.amount }.sum()
val outputAmount = outputs.map { it.amount }.sumOrZero(currency) val outputAmount = outputs.map { it.amount }.sumOrZero(currency)
val issuerCommand = args. val issuerCommand = args.select<ExitCashCommand>(institution = deposit.institution).singleOrNull()
filter { it.signingInstitution == deposit.institution }. val amountExitingLedger = issuerCommand?.value?.amount ?: Amount(0, inputAmount.currency)
// 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)
requireThat { requireThat {
"for deposit ${deposit.reference} at issuer ${deposit.institution.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger) "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 // see a signature from each of those keys. The actual signatures have been verified against the transaction
// data by the platform before execution. // data by the platform before execution.
val owningPubKeys = cashInputs.map { it.owner }.toSortedSet() val owningPubKeys = cashInputs.map { it.owner }.toSortedSet()
val keysThatSigned = args.filter { it.command is MoveCashCommand }.map { it.signer }.toSortedSet() val keysThatSigned = args.select<MoveCashCommand>().map { it.signer }.toSortedSet()
requireThat { requireThat {
"the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned) "the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned)
} }
@ -167,7 +167,7 @@ object CashContract : Contract {
} else states } else states
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet. // 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()) return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
} }

View File

@ -1,6 +1,7 @@
package core package core
import java.math.BigDecimal import java.math.BigDecimal
import java.security.PublicKey
import java.util.* import java.util.*
import kotlin.math.div 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) // TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354)
// region Misc
inline fun <reified T : Command> List<VerifiedSigned<Command>>.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<T>(it.signer, it.signingInstitution, it.value as T) }
inline fun <reified T : Command> List<VerifiedSigned<Command>>.requireSingleCommand() = select<T>().single()
// endregion
// region Currencies // region Currencies
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
fun currency(code: String) = Currency.getInstance(code) fun currency(code: String) = Currency.getInstance(code)
@ -46,7 +58,6 @@ fun requireThat(body: Requirements.() -> Unit) {
// endregion // endregion
// region Amounts // region Amounts
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -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) class LegallyIdentifiable(val signer: Institution, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
} }
object NullPublicKey : PublicKey, Comparable<PublicKey> {
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"
}

View File

@ -76,11 +76,11 @@ data class SignedCommand(
) )
/** Obtained from a [SignedCommand], deserialised and signature checked */ /** Obtained from a [SignedCommand], deserialised and signature checked */
data class VerifiedSignedCommand( data class VerifiedSigned<out T : Command>(
val signer: PublicKey, val signer: PublicKey,
/** If the public key was recognised, the looked up institution is available here, otherwise it's null */ /** If the public key was recognised, the looked up institution is available here, otherwise it's null */
val signingInstitution: Institution?, val signingInstitution: Institution?,
val command: Command val value: T
) )
/** /**
@ -90,13 +90,13 @@ data class VerifiedSignedCommand(
*/ */
interface Contract { interface Contract {
/** Must throw an exception if there's a problem that should prevent state transition. */ /** Must throw an exception if there's a problem that should prevent state transition. */
fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSignedCommand>) fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>)
} }
/** /**
* Reference to some money being stored by an institution e.g. in a vault or (more likely) on their normal ledger. * Reference to something being stored or issued by an institution e.g. in a vault or (more likely) on their normal
* The deposit reference is intended to be encrypted so it's meaningless to anyone other than the institution. * 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" override fun toString() = "${institution.name}$reference"
} }

View File

@ -50,11 +50,11 @@ val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Institution> = mapOf(
data class TransactionForTest( data class TransactionForTest(
private val inStates: MutableList<ContractState> = arrayListOf(), private val inStates: MutableList<ContractState> = arrayListOf(),
private val outStates: MutableList<ContractState> = arrayListOf(), private val outStates: MutableList<ContractState> = arrayListOf(),
private val args: MutableList<VerifiedSignedCommand> = arrayListOf() private val args: MutableList<VerifiedSigned<Command>> = arrayListOf()
) { ) {
fun input(s: () -> ContractState) = inStates.add(s()) fun input(s: () -> ContractState) = inStates.add(s())
fun output(s: () -> ContractState) = outStates.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) { infix fun Contract.`fails requirement`(msg: String) {
try { try {

View File

@ -1,3 +1,4 @@
import contracts.*
import core.* import core.*
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -9,7 +10,7 @@ import kotlin.test.assertFailsWith
class CashTests { class CashTests {
val inState = CashState( val inState = CashState(
deposit = DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), deposit = InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)),
amount = 1000.DOLLARS, amount = 1000.DOLLARS,
owner = DUMMY_PUBKEY_1 owner = DUMMY_PUBKEY_1
) )
@ -222,10 +223,10 @@ class CashTests {
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1 val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2 val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
val WALLET = listOf( val WALLET = listOf(
CashState(DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1), CashState(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1),
CashState(DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1), CashState(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1),
CashState(DepositPointer(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1), CashState(InstitutionReference(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(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1)
) )
@Test @Test