More moving around and renaming, to have a consistent style between contracts.

This commit is contained in:
Mike Hearn 2015-11-08 13:34:24 +01:00
parent 0856894047
commit 5022f11d9e
3 changed files with 74 additions and 69 deletions

View File

@ -11,37 +11,8 @@ import java.time.Instant
// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode.
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: InstitutionReference,
val amount: Amount,
/** There must be a MoveCommand signed by this key to claim the amount */
val owner: PublicKey
) : ContractState {
override val programRef = CASH_PROGRAM_ID
override fun toString() = "Cash($amount at $deposit owned by $owner)"
}
/** A command proving ownership of some input states, the signature covers the output states. */
class MoveCashCommand : Command {
override fun equals(other: Any?) = other is MoveCashCommand
override fun hashCode() = 0
}
/** A command stating that money has been withdrawn from the shared ledger and is now accounted for in some other way */
class ExitCashCommand(val amount: Amount) : Command {
override fun equals(other: Any?) = other is ExitCashCommand && other.amount == amount
override fun hashCode() = amount.hashCode()
}
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()
fun Iterable<ContractState>.sumCash() = this.filterIsInstance<CashState>().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
@ -55,10 +26,39 @@ fun Iterable<ContractState>.sumCash() = this.filterIsInstance<CashState>().map {
* At the same time, other contracts that just want money and don't care much who is currently holding it in their
* vaults can ignore the issuer/depositRefs and just examine the amount fields.
*/
object CashContract : Contract {
object Cash : Contract {
/** A state representing a claim on the cash reserves of some institution */
data class State(
/** Where the underlying currency backing this ledger entry can be found (propagated) */
val deposit: InstitutionReference,
val amount: Amount,
/** There must be a MoveCommand signed by this key to claim the amount */
val owner: PublicKey
) : ContractState {
override val programRef = CASH_PROGRAM_ID
override fun toString() = "Cash($amount at $deposit owned by $owner)"
}
sealed class Commands {
/** A command proving ownership of some input states, the signature covers the output states. */
class Move : Command {
override fun equals(other: Any?) = other is Move
override fun hashCode() = 0
}
/** A command stating that money has been withdrawn from the shared ledger and is now accounted for in some other way */
class Exit(val amount: Amount) : Command {
override fun equals(other: Any?) = other is Exit && other.amount == amount
override fun hashCode() = amount.hashCode()
}
}
/** This is the function EVERYONE runs */
override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>, time: Instant) {
val cashInputs = inStates.filterIsInstance<CashState>()
val cashInputs = inStates.filterIsInstance<Cash.State>()
requireThat {
"there is at least one cash input" by cashInputs.isNotEmpty()
@ -69,7 +69,7 @@ object CashContract : Contract {
val currency = cashInputs.first().amount.currency
// Select all the output states that are cash states. There may be zero if all money is being withdrawn.
val cashOutputs = outStates.filterIsInstance<CashState>()
val cashOutputs = outStates.filterIsInstance<Cash.State>()
requireThat {
"all outputs use the currency of the inputs" by cashOutputs.all { it.amount.currency == currency }
}
@ -84,7 +84,7 @@ object CashContract : Contract {
val inputAmount = inputs.map { it.amount }.sum()
val outputAmount = outputs.map { it.amount }.sumOrZero(currency)
val issuerCommand = args.select<ExitCashCommand>(institution = deposit.institution).singleOrNull()
val issuerCommand = args.select<Commands.Exit>(institution = deposit.institution).singleOrNull()
val amountExitingLedger = issuerCommand?.value?.amount ?: Amount(0, inputAmount.currency)
requireThat {
@ -98,7 +98,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.select<MoveCashCommand>().map { it.signer }.toSortedSet()
val keysThatSigned = args.select<Commands.Move>().map { it.signer }.toSortedSet()
requireThat {
"the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned)
}
@ -111,7 +111,7 @@ object CashContract : Contract {
/** Generate a transaction that consumes one or more of the given input states to move money to the given pubkey. */
@Throws(InsufficientBalanceException::class)
fun craftSpend(amount: Amount, to: PublicKey, wallet: List<CashState>): TransactionForTest {
fun craftSpend(amount: Amount, to: PublicKey, wallet: List<Cash.State>): TransactionForTest {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
@ -135,9 +135,9 @@ object CashContract : Contract {
// is put into the transaction, which is finally returned.
val currency = amount.currency
val coinsOfCurrency = wallet.asSequence().filter { it.amount.currency == currency }
val coinsOfCurrency = wallet.filter { it.amount.currency == currency }
val gathered = arrayListOf<CashState>()
val gathered = arrayListOf<State>()
var gatheredAmount = Amount(0, currency)
for (c in coinsOfCurrency) {
if (gatheredAmount >= amount) break
@ -154,7 +154,7 @@ object CashContract : Contract {
val states = gathered.groupBy { it.deposit }.map {
val (deposit, coins) = it
val totalAmount = coins.map { it.amount }.sum()
CashState(deposit, totalAmount, to)
State(deposit, totalAmount, to)
}
val outputs = if (change.pennies > 0) {
@ -165,12 +165,16 @@ object CashContract : Contract {
// Add a change output and adjust the last output downwards.
states.subList(0, states.lastIndex) +
states.last().let { it.copy(amount = it.amount - change) } +
CashState(gathered.last().deposit, change, changeKey)
State(gathered.last().deposit, change, changeKey)
} else states
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet.
val commands = keysUsed.map { VerifiedSigned(it, null, MoveCashCommand()) }
val commands = keysUsed.map { VerifiedSigned(it, null, Commands.Move()) }
return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
}
}
}
// Small DSL extension.
fun Iterable<ContractState>.sumCashBy(owner: PublicKey) = filterIsInstance<Cash.State>().filter { it.owner == owner }.map { it.amount }.sum()
fun Iterable<ContractState>.sumCash() = filterIsInstance<Cash.State>().map { it.amount }.sum()

View File

@ -21,13 +21,13 @@ import java.time.Instant
val CP_PROGRAM_ID = SecureHash.sha256("comedy-paper")
// TODO: Generalise the notion of an owned object into a superclass/supercontract. Consider composition vs inheritance.
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
object ComedyPaper : Contract {
data class State(
val issuance: InstitutionReference,
val owner: PublicKey,
val faceValue: Amount,
val maturityDate: Instant
val issuance: InstitutionReference,
val owner: PublicKey,
val faceValue: Amount,
val maturityDate: Instant
) : ContractState {
override val programRef = CP_PROGRAM_ID

View File

@ -1,4 +1,5 @@
import contracts.*
import contracts.Cash
import contracts.InsufficientBalanceException
import core.*
import org.junit.Test
import kotlin.test.assertEquals
@ -9,16 +10,16 @@ import kotlin.test.assertFailsWith
// 2. There must be at least one input state (note: not "one of the type the contract wants")
class CashTests {
val inState = CashState(
val inState = Cash.State(
deposit = InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)),
amount = 1000.DOLLARS,
owner = DUMMY_PUBKEY_1
)
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
val contract = CashContract
val contract = Cash
fun CashState.editInstitution(institution: Institution) = copy(deposit = deposit.copy(institution = institution))
fun CashState.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref)))
fun Cash.State.editInstitution(institution: Institution) = copy(deposit = deposit.copy(institution = institution))
fun Cash.State.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref)))
@Test
fun trivial() {
@ -39,7 +40,7 @@ class CashTests {
}
transaction {
output { outState }
arg(DUMMY_PUBKEY_2) { MoveCashCommand() }
arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
contract `fails requirement` "the owning keys are the same as the signing keys"
}
transaction {
@ -50,7 +51,7 @@ class CashTests {
// Simple reallocation works.
transaction {
output { outState }
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
contract.accepts()
}
}
@ -60,7 +61,7 @@ class CashTests {
fun testMergeSplit() {
// Splitting value works.
transaction {
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
transaction {
input { inState }
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
@ -150,16 +151,16 @@ class CashTests {
output { outState.copy(amount = inState.amount - 200.DOLLARS) }
transaction {
arg(MEGA_CORP_KEY) { ExitCashCommand(100.DOLLARS) }
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(100.DOLLARS) }
contract `fails requirement` "the amounts balance"
}
transaction {
arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
contract `fails requirement` "the owning keys are the same as the signing keys" // No move command.
transaction {
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
contract.accepts()
}
}
@ -172,14 +173,14 @@ class CashTests {
output { inState.copy(amount = inState.amount - 200.DOLLARS).editInstitution(MINI_CORP) }
output { inState.copy(amount = inState.amount - 200.DOLLARS) }
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
contract `fails requirement` "at issuer MegaCorp the amounts balance"
arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
contract `fails requirement` "at issuer MiniCorp the amounts balance"
arg(MINI_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
arg(MINI_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
contract.accepts()
}
}
@ -206,7 +207,7 @@ class CashTests {
// This works.
output { inState.copy(owner = DUMMY_PUBKEY_2) }
output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) }
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
contract.accepts()
}
@ -223,10 +224,10 @@ class CashTests {
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
val WALLET = listOf(
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)
Cash.State(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1),
Cash.State(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1),
Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1),
Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1)
)
@Test
@ -235,7 +236,7 @@ class CashTests {
transaction {
input { WALLET[0] }
output { WALLET[0].copy(owner = THEIR_PUBKEY_1) }
arg(OUR_PUBKEY_1) { MoveCashCommand() }
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
},
contract.craftSpend(100.DOLLARS, THEIR_PUBKEY_1, WALLET)
)
@ -248,7 +249,7 @@ class CashTests {
input { WALLET[0] }
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS) }
output { WALLET[0].copy(owner = OUR_PUBKEY_1, amount = 90.DOLLARS) }
arg(OUR_PUBKEY_1) { MoveCashCommand() }
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
},
contract.craftSpend(10.DOLLARS, THEIR_PUBKEY_1, WALLET)
)
@ -261,7 +262,7 @@ class CashTests {
input { WALLET[0] }
input { WALLET[1] }
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
arg(OUR_PUBKEY_1) { MoveCashCommand() }
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
},
contract.craftSpend(500.DOLLARS, THEIR_PUBKEY_1, WALLET)
)
@ -276,7 +277,7 @@ class CashTests {
input { WALLET[2] }
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
output { WALLET[2].copy(owner = THEIR_PUBKEY_1) }
arg(OUR_PUBKEY_1) { MoveCashCommand() }
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
},
contract.craftSpend(580.DOLLARS, THEIR_PUBKEY_1, WALLET)
)