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 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<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
* 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<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>()
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<ExitCashCommand>(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<MoveCashCommand>().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())
}

View File

@ -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 <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
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
fun currency(code: String) = Currency.getInstance(code)
@ -46,7 +58,6 @@ fun requireThat(body: Requirements.() -> Unit) {
// endregion
// 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)
}
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 */
data class VerifiedSignedCommand(
data class VerifiedSigned<out T : Command>(
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<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.
* 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"
}

View File

@ -50,11 +50,11 @@ val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Institution> = mapOf(
data class TransactionForTest(
private val inStates: 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 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 {

View File

@ -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