mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
Platform: fix the ability to summon cash in uncontrolled ways.
Introduce three ways to represent transactions: wire, ledger and for-verification. TransactionForVerification contains the arguments formally passed to verify(). Making Contract.verify() take only a single object as the argument makes it easier to evolve the Contract binary interface without breaking backwards compatibility with existing contracts that are uploaded to the ledger. The Kotlin "with" construct can be used to bring the members into scope, thus acting syntactically like function arguments. Make the contracts to be run depend on both input AND output states: this approach seems simpler than trying to have some notion of marking outputs that are checked. The TransactionForVerification class implements this simple logic (it's all moved into Transactions.kt).
This commit is contained in:
parent
4b9536af04
commit
12b7c7c184
@ -2,7 +2,6 @@ package contracts
|
|||||||
|
|
||||||
import core.*
|
import core.*
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
@ -64,52 +63,55 @@ object Cash : Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** This is the function EVERYONE runs */
|
/** This is the function EVERYONE runs */
|
||||||
override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>, time: Instant) {
|
override fun verify(tx: TransactionForVerification) {
|
||||||
val cashInputs = inStates.filterIsInstance<Cash.State>()
|
with(tx) {
|
||||||
|
val cashInputs = inStates.filterIsInstance<Cash.State>()
|
||||||
requireThat {
|
|
||||||
"there are no zero sized inputs" by cashInputs.none { it.amount.pennies == 0 }
|
|
||||||
"all inputs use the same currency" by (cashInputs.groupBy { it.amount.currency }.size == 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Cash.State>()
|
|
||||||
requireThat {
|
|
||||||
"all outputs use the currency of the inputs" by cashOutputs.all { it.amount.currency == currency }
|
|
||||||
}
|
|
||||||
|
|
||||||
// For each deposit that's represented in the inputs, group the inputs together and verify that the outputs
|
|
||||||
// balance, taking into account a possible exit command from that issuer.
|
|
||||||
var outputsLeft = cashOutputs.size
|
|
||||||
for ((deposit, inputs) in cashInputs.groupBy { it.deposit }) {
|
|
||||||
val outputs = cashOutputs.filter { it.deposit == deposit }
|
|
||||||
outputsLeft -= outputs.size
|
|
||||||
|
|
||||||
val inputAmount = inputs.map { it.amount }.sum()
|
|
||||||
val outputAmount = outputs.map { it.amount }.sumOrZero(currency)
|
|
||||||
|
|
||||||
val issuerCommand = args.select<Commands.Exit>(institution = deposit.institution).singleOrNull()
|
|
||||||
val amountExitingLedger = issuerCommand?.value?.amount ?: Amount(0, inputAmount.currency)
|
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"for deposit ${deposit.reference} at issuer ${deposit.institution.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger)
|
"there is at least one cash input" by cashInputs.isNotEmpty()
|
||||||
|
"there are no zero sized inputs" by cashInputs.none { it.amount.pennies == 0 }
|
||||||
|
"all inputs use the same currency" by (cashInputs.groupBy { it.amount.currency }.size == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<Cash.State>()
|
||||||
|
requireThat {
|
||||||
|
"all outputs use the currency of the inputs" by cashOutputs.all { it.amount.currency == currency }
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each deposit that's represented in the inputs, group the inputs together and verify that the outputs
|
||||||
|
// balance, taking into account a possible exit command from that issuer.
|
||||||
|
var outputsLeft = cashOutputs.size
|
||||||
|
for ((deposit, inputs) in cashInputs.groupBy { it.deposit }) {
|
||||||
|
val outputs = cashOutputs.filter { it.deposit == deposit }
|
||||||
|
outputsLeft -= outputs.size
|
||||||
|
|
||||||
|
val inputAmount = inputs.map { it.amount }.sum()
|
||||||
|
val outputAmount = outputs.map { it.amount }.sumOrZero(currency)
|
||||||
|
|
||||||
|
val issuerCommand = args.select<Commands.Exit>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requireThat { "no output states are unaccounted for" by (outputsLeft == 0) }
|
||||||
|
|
||||||
|
// Now check the digital signatures on the move commands. Every input has an owning public key, and we must
|
||||||
|
// 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<Commands.Move>().map { it.signer }.toSortedSet()
|
||||||
|
requireThat {
|
||||||
|
"the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept.
|
||||||
}
|
}
|
||||||
|
|
||||||
requireThat { "no output states are unaccounted for" by (outputsLeft == 0) }
|
|
||||||
|
|
||||||
// Now check the digital signatures on the move commands. Every input has an owning public key, and we must
|
|
||||||
// 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<Commands.Move>().map { it.signer }.toSortedSet()
|
|
||||||
requireThat {
|
|
||||||
"the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: craftSpend should work more like in bitcoinj, where it takes and modifies a transaction template.
|
// TODO: craftSpend should work more like in bitcoinj, where it takes and modifies a transaction template.
|
||||||
|
@ -41,28 +41,30 @@ object ComedyPaper : Contract {
|
|||||||
class Redeem : Commands()
|
class Redeem : Commands()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>, time: Instant) {
|
override fun verify(tx: TransactionForVerification) {
|
||||||
// There are two possible things that can be done with CP. The first is trading it. The second is redeeming it
|
with(tx) {
|
||||||
// for cash on or after the maturity date.
|
// There are two possible things that can be done with CP. The first is trading it. The second is redeeming it
|
||||||
val command = args.requireSingleCommand<ComedyPaper.Commands>()
|
// for cash on or after the maturity date.
|
||||||
|
val command = args.requireSingleCommand<ComedyPaper.Commands>()
|
||||||
|
|
||||||
// For now do not allow multiple pieces of CP to trade in a single transaction. Study this more!
|
// For now do not allow multiple pieces of CP to trade in a single transaction. Study this more!
|
||||||
val input = inStates.filterIsInstance<ComedyPaper.State>().single()
|
val input = inStates.filterIsInstance<ComedyPaper.State>().single()
|
||||||
|
|
||||||
when (command.value) {
|
when (command.value) {
|
||||||
is Commands.Move -> requireThat {
|
is Commands.Move -> requireThat {
|
||||||
val output = outStates.filterIsInstance<ComedyPaper.State>().single()
|
val output = outStates.filterIsInstance<ComedyPaper.State>().single()
|
||||||
"the transaction is signed by the owner of the CP" by (command.signer == input.owner)
|
"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())
|
"the output state is the same as the input state except for owner" by (input.withoutOwner() == output.withoutOwner())
|
||||||
}
|
}
|
||||||
|
|
||||||
is Commands.Redeem -> requireThat {
|
is Commands.Redeem -> requireThat {
|
||||||
val received = outStates.sumCash()
|
val received = outStates.sumCash()
|
||||||
// Do we need to check the signature of the issuer here too?
|
// Do we need to check the signature of the issuer here too?
|
||||||
"the transaction is signed by the owner of the CP" by (command.signer == input.owner)
|
"the transaction is signed by the owner of the CP" by (command.signer == input.owner)
|
||||||
"the paper must have matured" by (input.maturityDate < time)
|
"the paper must have matured" by (input.maturityDate < time)
|
||||||
"the received amount equals the face value" by (received == input.faceValue)
|
"the received amount equals the face value" by (received == input.faceValue)
|
||||||
"the paper must be destroyed" by outStates.filterIsInstance<ComedyPaper.State>().none()
|
"the paper must be destroyed" by outStates.filterIsInstance<ComedyPaper.State>().none()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
src/contracts/DummyContract.kt
Normal file
22
src/contracts/DummyContract.kt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package contracts
|
||||||
|
|
||||||
|
import core.Contract
|
||||||
|
import core.ContractState
|
||||||
|
import core.SecureHash
|
||||||
|
import core.TransactionForVerification
|
||||||
|
|
||||||
|
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||||
|
|
||||||
|
val DUMMY_PROGRAM_ID = SecureHash.sha256("dummy")
|
||||||
|
|
||||||
|
object DummyContract : Contract {
|
||||||
|
class State : ContractState {
|
||||||
|
override val programRef: SecureHash = DUMMY_PROGRAM_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verify(tx: TransactionForVerification) {
|
||||||
|
// Always accepts.
|
||||||
|
}
|
||||||
|
|
||||||
|
override val legalContractReference: String = "/dev/null"
|
||||||
|
}
|
@ -2,7 +2,6 @@ package core
|
|||||||
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.Timestamp
|
import java.security.Timestamp
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk
|
* A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk
|
||||||
@ -23,21 +22,6 @@ interface ContractState {
|
|||||||
*/
|
*/
|
||||||
class ContractStateRef(private val hash: SecureHash.SHA256)
|
class ContractStateRef(private val hash: SecureHash.SHA256)
|
||||||
|
|
||||||
/**
|
|
||||||
* A transaction wraps the data needed to calculate one or more successor states from a set of input states.
|
|
||||||
* The data here is provided in lightly processed form to the verify method of each input states contract program.
|
|
||||||
* Specifically, the input state refs are dereferenced into real [ContractState]s and the args are signature checked
|
|
||||||
* and institutions are looked up (if known).
|
|
||||||
*/
|
|
||||||
class Transaction(
|
|
||||||
/** Arbitrary data passed to the program of each input state. */
|
|
||||||
val args: List<SignedCommand>,
|
|
||||||
/** The input states which will be consumed/invalidated by the execution of this transaction. */
|
|
||||||
val inputStates: List<ContractStateRef>,
|
|
||||||
/** The states that will be generated by the execution of this transaction. */
|
|
||||||
val outputStates: List<ContractState>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A transition groups one or more transactions together, and combines them with a signed timestamp. A transaction
|
* A transition groups one or more transactions together, and combines them with a signed timestamp. A transaction
|
||||||
* may not stand independent of a transition and all transactions are applied or reverted together as a unit.
|
* may not stand independent of a transition and all transactions are applied or reverted together as a unit.
|
||||||
@ -48,7 +32,7 @@ class Transaction(
|
|||||||
* will always win.
|
* will always win.
|
||||||
*/
|
*/
|
||||||
data class Transition(
|
data class Transition(
|
||||||
val tx: Transaction,
|
val tx: LedgerTransaction,
|
||||||
|
|
||||||
/** Timestamp of the serialised transaction as fetched from a timestamping authority (RFC 3161) */
|
/** Timestamp of the serialised transaction as fetched from a timestamping authority (RFC 3161) */
|
||||||
val signedTimestamp: Timestamp
|
val signedTimestamp: Timestamp
|
||||||
@ -86,13 +70,18 @@ data class VerifiedSigned<out T : Command>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for
|
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for
|
||||||
* every [Transaction] they see on the network, for every input state. All input states must accept the transaction
|
* every [LedgerTransaction] they see on the network, for every input state. All input states must accept the transaction
|
||||||
* for it to be accepted: failure of any aborts the entire thing. The time is taken from a trusted timestamp attached
|
* for it to be accepted: failure of any aborts the entire thing. The time is taken from a trusted timestamp attached
|
||||||
* to the transaction itself i.e. it is NOT necessarily the current time.
|
* to the transaction itself i.e. it is NOT necessarily the current time.
|
||||||
*/
|
*/
|
||||||
interface Contract {
|
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<VerifiedSigned<Command>>, time: Instant)
|
* Takes an object that represents a state transition, and ensures the inputs/outputs/commands make sense.
|
||||||
|
* Must throw an exception if there's a problem that should prevent state transition. Takes a single object
|
||||||
|
* rather than an argument so that additional data can be added without breaking binary compatibility with
|
||||||
|
* existing contract code.
|
||||||
|
*/
|
||||||
|
fun verify(tx: TransactionForVerification)
|
||||||
|
|
||||||
// TODO: This should probably be a hash of a document, rather than a URL to it.
|
// TODO: This should probably be a hash of a document, rather than a URL to it.
|
||||||
/** Unparsed reference to the natural language contract that this code is supposed to express (usually a URL). */
|
/** Unparsed reference to the natural language contract that this code is supposed to express (usually a URL). */
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
|
import contracts.*
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -29,6 +30,14 @@ val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Institution> = mapOf(
|
|||||||
// A dummy time at which we will be pretending test transactions are created.
|
// 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")
|
val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||||
|
|
||||||
|
// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside
|
||||||
|
// a sandbox. For now we just instantiate right at the start of the program.
|
||||||
|
val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
|
||||||
|
CASH_PROGRAM_ID to Cash,
|
||||||
|
CP_PROGRAM_ID to ComedyPaper,
|
||||||
|
DUMMY_PROGRAM_ID to DummyContract
|
||||||
|
)
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
|
// DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
|
||||||
@ -60,9 +69,11 @@ data class TransactionForTest(
|
|||||||
fun output(s: () -> ContractState) = outStates.add(s())
|
fun output(s: () -> ContractState) = outStates.add(s())
|
||||||
fun arg(key: PublicKey, c: () -> Command) = args.add(VerifiedSigned(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) {
|
private fun run() = TransactionForVerification(inStates, outStates, args, TEST_TX_TIME).verify(TEST_PROGRAM_MAP)
|
||||||
|
|
||||||
|
infix fun `fails requirement`(msg: String) {
|
||||||
try {
|
try {
|
||||||
verify(inStates, outStates, args, TEST_TX_TIME)
|
run()
|
||||||
} catch(e: Exception) {
|
} catch(e: Exception) {
|
||||||
val m = e.message
|
val m = e.message
|
||||||
if (m == null)
|
if (m == null)
|
||||||
@ -73,12 +84,12 @@ data class TransactionForTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// which is uglier?? :)
|
// which is uglier?? :)
|
||||||
fun Contract.fails_requirement(msg: String) = this.`fails requirement`(msg)
|
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
|
||||||
|
|
||||||
fun Contract.accepts() = verify(inStates, outStates, args, TEST_TX_TIME)
|
fun accepts() = run()
|
||||||
fun Contract.rejects(withMessage: String? = null) {
|
fun rejects(withMessage: String? = null) {
|
||||||
val r = try {
|
val r = try {
|
||||||
accepts()
|
run()
|
||||||
false
|
false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val m = e.message
|
val m = e.message
|
||||||
|
50
src/core/Transactions.kt
Normal file
50
src/core/Transactions.kt
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
// Various views of transactions as they progress through the pipeline:
|
||||||
|
//
|
||||||
|
// WireTransaction -> LedgerTransaction -> TransactionForVerification
|
||||||
|
// TransactionForTest
|
||||||
|
|
||||||
|
class WireTransaction {
|
||||||
|
// TODO: This is supposed to be a protocol buffer, FIX SPE message, etc. For prototype it can just be Kryo serialised
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states.
|
||||||
|
* It is the first step after extraction
|
||||||
|
*/
|
||||||
|
class LedgerTransaction(
|
||||||
|
/** The input states which will be consumed/invalidated by the execution of this transaction. */
|
||||||
|
val inputStates: List<ContractStateRef>,
|
||||||
|
/** The states that will be generated by the execution of this transaction. */
|
||||||
|
val outputStates: List<ContractState>,
|
||||||
|
/** Arbitrary data passed to the program of each input state. */
|
||||||
|
val args: List<SignedCommand>,
|
||||||
|
/** The moment the transaction was timestamped for */
|
||||||
|
val time: Instant
|
||||||
|
)
|
||||||
|
|
||||||
|
/** A transaction in fully resolved form, ready for passing as input to a verification function */
|
||||||
|
class TransactionForVerification(
|
||||||
|
val inStates: List<ContractState>,
|
||||||
|
val outStates: List<ContractState>,
|
||||||
|
val args: List<VerifiedSigned<Command>>,
|
||||||
|
val time: Instant
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun verify(programMap: Map<SecureHash, Contract>) {
|
||||||
|
// For each input and output state, locate the program to run. Then execute the verification function. If any
|
||||||
|
// throws an exception, the entire transaction is invalid.
|
||||||
|
fun runContracts(desc: String, l: List<ContractState>) {
|
||||||
|
for ((index, state) in l.withIndex()) {
|
||||||
|
val contract = programMap[state.programRef] ?: throw IllegalStateException("$desc state $index refers to unknown contract ${state.programRef}")
|
||||||
|
contract.verify(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runContracts("input", inStates)
|
||||||
|
runContracts("output", outStates)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import contracts.Cash
|
import contracts.Cash
|
||||||
|
import contracts.DummyContract
|
||||||
import contracts.InsufficientBalanceException
|
import contracts.InsufficientBalanceException
|
||||||
import core.*
|
import core.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -16,7 +17,6 @@ class CashTests {
|
|||||||
owner = DUMMY_PUBKEY_1
|
owner = DUMMY_PUBKEY_1
|
||||||
)
|
)
|
||||||
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
||||||
val contract = Cash
|
|
||||||
|
|
||||||
fun Cash.State.editInstitution(institution: Institution) = copy(deposit = deposit.copy(institution = institution))
|
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)))
|
fun Cash.State.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref)))
|
||||||
@ -25,36 +25,47 @@ class CashTests {
|
|||||||
fun trivial() {
|
fun trivial() {
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
contract `fails requirement` "the amounts balance"
|
this `fails requirement` "the amounts balance"
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
output { outState.copy(amount = 2000.DOLLARS )}
|
output { outState.copy(amount = 2000.DOLLARS )}
|
||||||
contract `fails requirement` "the amounts balance"
|
this `fails requirement` "the amounts balance"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
output { outState }
|
output { outState }
|
||||||
// No command arguments
|
// No command arguments
|
||||||
contract `fails requirement` "the owning keys are the same as the signing keys"
|
this `fails requirement` "the owning keys are the same as the signing keys"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
||||||
contract `fails requirement` "the owning keys are the same as the signing keys"
|
this `fails requirement` "the owning keys are the same as the signing keys"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
output { outState }
|
output { outState }
|
||||||
output { outState.editInstitution(MINI_CORP) }
|
output { outState.editInstitution(MINI_CORP) }
|
||||||
contract `fails requirement` "no output states are unaccounted for"
|
this `fails requirement` "no output states are unaccounted for"
|
||||||
}
|
}
|
||||||
// Simple reallocation works.
|
// Simple reallocation works.
|
||||||
transaction {
|
transaction {
|
||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
contract.accepts()
|
this.accepts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCannotSummonMoney() {
|
||||||
|
// Make sure that the contract runs even if there are no cash input states.
|
||||||
|
transaction {
|
||||||
|
input { DummyContract.State() }
|
||||||
|
output { outState }
|
||||||
|
|
||||||
|
this `fails requirement` "there is at least one cash input"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMergeSplit() {
|
fun testMergeSplit() {
|
||||||
// Splitting value works.
|
// Splitting value works.
|
||||||
@ -63,21 +74,21 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
|
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
|
||||||
contract.accepts()
|
this.accepts()
|
||||||
}
|
}
|
||||||
// Merging 4 inputs into 2 outputs works.
|
// Merging 4 inputs into 2 outputs works.
|
||||||
transaction {
|
transaction {
|
||||||
for (i in 1..4) input { inState.copy(amount = inState.amount / 4) }
|
for (i in 1..4) input { inState.copy(amount = inState.amount / 4) }
|
||||||
output { inState.copy(amount = inState.amount / 2) }
|
output { inState.copy(amount = inState.amount / 2) }
|
||||||
output { inState.copy(amount = inState.amount / 2) }
|
output { inState.copy(amount = inState.amount / 2) }
|
||||||
contract.accepts()
|
this.accepts()
|
||||||
}
|
}
|
||||||
// Merging 2 inputs into 1 works.
|
// Merging 2 inputs into 1 works.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState.copy(amount = inState.amount / 2) }
|
input { inState.copy(amount = inState.amount / 2) }
|
||||||
input { inState.copy(amount = inState.amount / 2) }
|
input { inState.copy(amount = inState.amount / 2) }
|
||||||
output { inState }
|
output { inState }
|
||||||
contract.accepts()
|
this.accepts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +99,7 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.copy(amount = 0.DOLLARS) }
|
input { inState.copy(amount = 0.DOLLARS) }
|
||||||
contract `fails requirement` "zero sized inputs"
|
this `fails requirement` "zero sized inputs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,21 +109,21 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState.editInstitution(MINI_CORP) }
|
output { outState.editInstitution(MINI_CORP) }
|
||||||
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't change deposit reference when splitting.
|
// Can't change deposit reference when splitting.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState.editDepositRef(0).copy(amount = inState.amount / 2) }
|
output { outState.editDepositRef(0).copy(amount = inState.amount / 2) }
|
||||||
output { outState.editDepositRef(1).copy(amount = inState.amount / 2) }
|
output { outState.editDepositRef(1).copy(amount = inState.amount / 2) }
|
||||||
contract `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
|
this `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't mix currencies.
|
// Can't mix currencies.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState.copy(amount = 800.DOLLARS) }
|
output { outState.copy(amount = 800.DOLLARS) }
|
||||||
output { outState.copy(amount = 200.POUNDS) }
|
output { outState.copy(amount = 200.POUNDS) }
|
||||||
contract `fails requirement` "all outputs use the currency of the inputs"
|
this `fails requirement` "all outputs use the currency of the inputs"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
@ -123,21 +134,21 @@ class CashTests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
output { outState.copy(amount = 1150.DOLLARS) }
|
output { outState.copy(amount = 1150.DOLLARS) }
|
||||||
contract `fails requirement` "all inputs use the same currency"
|
this `fails requirement` "all inputs use the same currency"
|
||||||
}
|
}
|
||||||
// Can't have superfluous input states from different issuers.
|
// Can't have superfluous input states from different issuers.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.editInstitution(MINI_CORP) }
|
input { inState.editInstitution(MINI_CORP) }
|
||||||
output { outState }
|
output { outState }
|
||||||
contract `fails requirement` "at issuer MiniCorp the amounts balance"
|
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't combine two different deposits at the same issuer.
|
// Can't combine two different deposits at the same issuer.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.editDepositRef(3) }
|
input { inState.editDepositRef(3) }
|
||||||
output { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
|
output { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
|
||||||
contract `fails requirement` "for deposit [01]"
|
this `fails requirement` "for deposit [01]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,16 +161,16 @@ class CashTests {
|
|||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(100.DOLLARS) }
|
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(100.DOLLARS) }
|
||||||
contract `fails requirement` "the amounts balance"
|
this `fails requirement` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(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.
|
this `fails requirement` "the owning keys are the same as the signing keys" // No move command.
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
contract.accepts()
|
this.accepts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,13 +184,13 @@ class CashTests {
|
|||||||
|
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
|
|
||||||
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||||
|
|
||||||
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
|
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||||
contract `fails requirement` "at issuer MiniCorp the amounts balance"
|
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
||||||
|
|
||||||
arg(MINI_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
|
arg(MINI_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||||
contract.accepts()
|
this.accepts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,20 +204,20 @@ class CashTests {
|
|||||||
// Can't merge them together.
|
// Can't merge them together.
|
||||||
transaction {
|
transaction {
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS) }
|
||||||
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Missing MiniCorp deposit
|
// Missing MiniCorp deposit
|
||||||
transaction {
|
transaction {
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||||
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
// This works.
|
// This works.
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) }
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
contract.accepts()
|
this.accepts()
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
@ -236,7 +247,7 @@ class CashTests {
|
|||||||
output { WALLET[0].copy(owner = THEIR_PUBKEY_1) }
|
output { WALLET[0].copy(owner = THEIR_PUBKEY_1) }
|
||||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
},
|
},
|
||||||
contract.craftSpend(100.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
Cash.craftSpend(100.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,7 +260,7 @@ class CashTests {
|
|||||||
output { WALLET[0].copy(owner = OUR_PUBKEY_1, amount = 90.DOLLARS) }
|
output { WALLET[0].copy(owner = OUR_PUBKEY_1, amount = 90.DOLLARS) }
|
||||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
},
|
},
|
||||||
contract.craftSpend(10.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
Cash.craftSpend(10.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,7 +273,7 @@ class CashTests {
|
|||||||
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
|
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
|
||||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
},
|
},
|
||||||
contract.craftSpend(500.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
Cash.craftSpend(500.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,17 +288,20 @@ class CashTests {
|
|||||||
output { WALLET[2].copy(owner = THEIR_PUBKEY_1) }
|
output { WALLET[2].copy(owner = THEIR_PUBKEY_1) }
|
||||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
},
|
},
|
||||||
contract.craftSpend(580.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
Cash.craftSpend(580.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun craftSpendInsufficientBalance() {
|
fun craftSpendInsufficientBalance() {
|
||||||
assertFailsWith(InsufficientBalanceException::class) {
|
try {
|
||||||
contract.craftSpend(1000.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
Cash.craftSpend(1000.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
||||||
|
assert(false)
|
||||||
|
} catch (e: InsufficientBalanceException) {
|
||||||
|
assertEquals(1000 - 580, e.amountMissing.pennies / 100)
|
||||||
}
|
}
|
||||||
assertFailsWith(InsufficientBalanceException::class) {
|
assertFailsWith(InsufficientBalanceException::class) {
|
||||||
contract.craftSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1, WALLET)
|
Cash.craftSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1, WALLET)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,6 @@ import org.junit.Test
|
|||||||
// TODO: Finish this off.
|
// TODO: Finish this off.
|
||||||
|
|
||||||
class ComedyPaperTests {
|
class ComedyPaperTests {
|
||||||
val contract = ComedyPaper
|
|
||||||
|
|
||||||
val PAPER_1 = ComedyPaper.State(
|
val PAPER_1 = ComedyPaper.State(
|
||||||
issuance = InstitutionReference(MEGA_CORP, OpaqueBytes.of(123)),
|
issuance = InstitutionReference(MEGA_CORP, OpaqueBytes.of(123)),
|
||||||
owner = DUMMY_PUBKEY_1,
|
owner = DUMMY_PUBKEY_1,
|
||||||
@ -23,16 +21,16 @@ class ComedyPaperTests {
|
|||||||
input { PAPER_1 }
|
input { PAPER_1 }
|
||||||
output { PAPER_2 }
|
output { PAPER_2 }
|
||||||
|
|
||||||
contract.rejects()
|
this.rejects()
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
arg(DUMMY_PUBKEY_2) { ComedyPaper.Commands.Move() }
|
arg(DUMMY_PUBKEY_2) { ComedyPaper.Commands.Move() }
|
||||||
contract `fails requirement` "is signed by the owner"
|
this `fails requirement` "is signed by the owner"
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
arg(DUMMY_PUBKEY_1) { ComedyPaper.Commands.Move() }
|
arg(DUMMY_PUBKEY_1) { ComedyPaper.Commands.Move() }
|
||||||
contract.accepts()
|
this.accepts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user