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 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 */ /** This is the function EVERYONE runs */
override fun verify(tx: TransactionForVerification) { override fun verify(tx: TransactionForVerification) {
// Each group is a set of input/output states with distinct (deposit, currency) attributes. These types // 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. // 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) { for ((inputs, outputs) in groups) {
requireThat { requireThat {
"all outputs represent at least one penny" by outputs.none { it.amount.pennies == 0 } "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: * Open issues:
* - In this model, you cannot merge or split CP. Can you do this normally? We could model CP as a specialised form * - 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? * 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") val CP_PROGRAM_ID = SecureHash.sha256("replace-me-later-with-bytecode-hash")
@ -44,30 +42,31 @@ object CommercialPaper : Contract {
} }
override fun verify(tx: TransactionForVerification) { override fun verify(tx: TransactionForVerification) {
with(tx) { // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
// There are two possible things that can be done with CP. The first is trading it. The second is redeeming it val groups = groupStates<State>(tx.inStates, tx.outStates) { it.withoutOwner() }
// for cash on or after the maturity date.
val command = commands.requireSingleCommand<CommercialPaper.Commands>()
// For now do not allow multiple pieces of CP to trade in a single transaction. Study this more! // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
val input = inStates.filterIsInstance<CommercialPaper.State>().single() // 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 { requireThat {
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner)) "the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
} }
val output = group.outputs.singleOrNull()
when (command.value) { when (command.value) {
is Commands.Move -> requireThat { is Commands.Move -> requireThat { "the output state is present" by (output != null) }
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.Redeem -> requireThat { is Commands.Redeem -> {
val received = outStates.sumCashOrNull() ?: throw IllegalStateException("no cash being redeemed") val received = tx.outStates.sumCashOrNull() ?: throw IllegalStateException("no cash being redeemed")
// Do we need to check the signature of the issuer here too? requireThat {
"the paper must have matured" by (input.maturityDate < time) // Do we need to check the signature of the issuer here too?
"the received amount equals the face value" by (received == input.faceValue) "the paper must have matured" by (input.maturityDate < tx.time)
"the paper must be destroyed" by outStates.filterIsInstance<CommercialPaper.State>().none() "the received amount equals the face value" by (received == input.faceValue)
"the paper must be destroyed" by (output == null)
}
} }
} }
} }

View File

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

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
}