Contracts API: move the notion of grouping into a utility file, and modify the commercial paper contract (java+kotlin) to use it.

This resolves several open TODO list items around the composability of contracts.

The current CP tests pass, but they aren't complete, so this doesn't prove the changes are correct. That'll come soon.
This commit is contained in:
Mike Hearn 2015-11-20 19:25:15 +01:00
parent d6cfa9b9ef
commit a8e34a2bb2
4 changed files with 110 additions and 62 deletions

View File

@ -62,32 +62,12 @@ object Cash : Contract {
data class Exit(val amount: Amount) : Command
}
data class InOutGroup(val inputs: List<Cash.State>, val outputs: List<Cash.State>)
private fun groupStates(allInputs: List<ContractState>, allOutputs: List<ContractState>): List<InOutGroup> {
val inputs = allInputs.filterIsInstance<Cash.State>()
val outputs = allOutputs.filterIsInstance<Cash.State>()
val inGroups = inputs.groupBy { Pair(it.deposit, it.amount.currency) }
val outGroups = outputs.groupBy { Pair(it.deposit, it.amount.currency) }
val result = ArrayList<InOutGroup>()
for ((k, v) in inGroups.entries)
result.add(InOutGroup(v, outGroups[k] ?: emptyList()))
for ((k, v) in outGroups.entries) {
if (inGroups[k] == null)
result.add(InOutGroup(emptyList(), v))
}
return result
}
/** This is the function EVERYONE runs */
override fun verify(tx: TransactionForVerification) {
// Each group is a set of input/output states with distinct (deposit, currency) attributes. These types
// of cash are not fungible and must be kept separated for bookkeeping purposes.
val groups = groupStates(tx.inStates, tx.outStates)
val groups = groupStates<Cash.State>(tx.inStates, tx.outStates) { Pair(it.deposit, it.amount.currency) }
for ((inputs, outputs) in groups) {
requireThat {
"all outputs represent at least one penny" by outputs.none { it.amount.pennies == 0 }

View File

@ -17,8 +17,6 @@ import java.time.Instant
* Open issues:
* - In this model, you cannot merge or split CP. Can you do this normally? We could model CP as a specialised form
* of cash, or reuse some of the cash code?
* - Currently cannot trade more than one piece of CP in a single transaction. This is probably going to be a common
* issue: need to find a cleaner way to allow this. Does the single-execution-per-transaction model make sense?
*/
val CP_PROGRAM_ID = SecureHash.sha256("replace-me-later-with-bytecode-hash")
@ -44,30 +42,31 @@ object CommercialPaper : Contract {
}
override fun verify(tx: TransactionForVerification) {
with(tx) {
// There are two possible things that can be done with CP. The first is trading it. The second is redeeming it
// for cash on or after the maturity date.
val command = commands.requireSingleCommand<CommercialPaper.Commands>()
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
val groups = groupStates<State>(tx.inStates, tx.outStates) { it.withoutOwner() }
// For now do not allow multiple pieces of CP to trade in a single transaction. Study this more!
val input = inStates.filterIsInstance<CommercialPaper.State>().single()
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
// it for cash on or after the maturity date.
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
for (group in groups) {
val input = group.inputs.single()
requireThat {
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
}
val output = group.outputs.singleOrNull()
when (command.value) {
is Commands.Move -> requireThat {
val output = outStates.filterIsInstance<CommercialPaper.State>().single()
"the output state is the same as the input state except for owner" by (input.withoutOwner() == output.withoutOwner())
}
is Commands.Move -> requireThat { "the output state is present" by (output != null) }
is Commands.Redeem -> requireThat {
val received = outStates.sumCashOrNull() ?: throw IllegalStateException("no cash being redeemed")
is Commands.Redeem -> {
val received = tx.outStates.sumCashOrNull() ?: throw IllegalStateException("no cash being redeemed")
requireThat {
// Do we need to check the signature of the issuer here too?
"the paper must have matured" by (input.maturityDate < time)
"the paper must have matured" by (input.maturityDate < tx.time)
"the received amount equals the face value" by (received == input.faceValue)
"the paper must be destroyed" by outStates.filterIsInstance<CommercialPaper.State>().none()
"the paper must be destroyed" by (output == null)
}
}
}
}

View File

@ -2,10 +2,12 @@ package contracts;
import core.*;
import core.serialization.*;
import kotlin.*;
import org.jetbrains.annotations.*;
import java.security.*;
import java.time.*;
import java.util.*;
import static core.ContractsDSLKt.*;
import static kotlin.CollectionsKt.*;
@ -17,7 +19,7 @@ import static kotlin.CollectionsKt.*;
* NOTE: For illustration only. Not unit tested.
*/
public class JavaCommercialPaper implements Contract {
public static class State implements SerializeableWithKryo {
public static class State implements ContractState, SerializeableWithKryo {
private InstitutionReference issuance;
private PublicKey owner;
private Amount faceValue;
@ -47,6 +49,12 @@ public class JavaCommercialPaper implements Contract {
public Instant getMaturityDate() {
return maturityDate;
}
@NotNull
@Override
public SecureHash getProgramRef() {
return SecureHash.Companion.sha256("java commercial paper (this should be a bytecode hash)");
}
}
public static class Commands implements core.Command {
@ -69,34 +77,43 @@ public class JavaCommercialPaper implements Contract {
public void verify(@NotNull TransactionForVerification tx) {
// There are two possible things that can be done with CP. The first is trading it. The second is redeeming it
// for cash on or after the maturity date.
List<InOutGroup<State>> groups = ContractTools.groupStates(State.class, tx.getInStates(), tx.getOutStates(),
state -> new Pair<>(state.getIssuance(), state.faceValue.getCurrency()));
// Find the command that instructs us what to do and check there's exactly one.
AuthenticatedObject<Command> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
for (InOutGroup<State> group : groups) {
List<State> inputs = group.getInputs();
List<State> outputs = group.getOutputs();
// For now do not allow multiple pieces of CP to trade in a single transaction. Study this more!
State input = single(filterIsInstance(tx.getInStates(), State.class));
State input = single(filterIsInstance(inputs, State.class));
if (!cmd.getSigners().contains(input.getOwner()))
throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP");
if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Move) {
// Check the output CP state is the same as the input state, ignoring the owner field.
State output = single(filterIsInstance(tx.getOutStates(), State.class));
State output = single(outputs);
if (!output.getFaceValue().equals(input.getFaceValue()) ||
!output.getIssuance().equals(input.getIssuance()) ||
!output.getMaturityDate().equals(input.getMaturityDate()))
throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner");
} else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) {
Amount received = CashKt.sumCashOrNull(tx.getInStates());
Amount received = CashKt.sumCashOrNull(inputs);
if (received == null)
throw new IllegalStateException("Failed requirement: no cash being redeemed");
if (input.getMaturityDate().isAfter(tx.getTime()))
throw new IllegalStateException("Failed requirement: the paper must have matured");
if (!input.getFaceValue().equals(received))
throw new IllegalStateException("Failed requirement: the received amount equals the face value");
if (!filterIsInstance(tx.getOutStates(), State.class).isEmpty())
if (!outputs.isEmpty())
throw new IllegalStateException("Failed requirement: the paper must be destroyed");
}
}
}
@NotNull
@Override

52
src/core/ContractTools.kt Normal file
View File

@ -0,0 +1,52 @@
@file:JvmName("ContractTools")
package core
import java.util.*
/**
* Utilities for contract writers to incorporate into their logic.
*/
data class InOutGroup<T : ContractState>(val inputs: List<T>, val outputs: List<T>)
// For Java users.
fun <T : ContractState> groupStates(ofType: Class<T>, allInputs: List<ContractState>,
allOutputs: List<ContractState>, selector: (T) -> Any): List<InOutGroup<T>> {
val inputs = allInputs.filterIsInstance(ofType)
val outputs = allOutputs.filterIsInstance(ofType)
val inGroups = inputs.groupBy(selector)
val outGroups = outputs.groupBy(selector)
@Suppress("DEPRECATION")
return groupStatesInternal(inGroups, outGroups)
}
// For Kotlin users: this version has nicer syntax and avoids reflection/object creation for the lambda.
inline fun <reified T : ContractState> groupStates(allInputs: List<ContractState>,
allOutputs: List<ContractState>,
selector: (T) -> Any): List<InOutGroup<T>> {
val inputs = allInputs.filterIsInstance<T>()
val outputs = allOutputs.filterIsInstance<T>()
val inGroups = inputs.groupBy(selector)
val outGroups = outputs.groupBy(selector)
@Suppress("DEPRECATION")
return groupStatesInternal(inGroups, outGroups)
}
@Deprecated("Do not use this directly: exposed as public only due to function inlining")
fun <T : ContractState> groupStatesInternal(inGroups: Map<Any, List<T>>, outGroups: Map<Any, List<T>>): List<InOutGroup<T>> {
val result = ArrayList<InOutGroup<T>>()
for ((k, v) in inGroups.entries)
result.add(InOutGroup(v, outGroups[k] ?: emptyList()))
for ((k, v) in outGroups.entries) {
if (inGroups[k] == null)
result.add(InOutGroup(emptyList(), v))
}
return result
}