Small cleanup of commands.

1. Rename Command -> CommandData, WireCommand -> Command, PartialTransaction.addArg -> addCommand
2. Add some helper functions to PartialTransaction to make creation of transactions simpler.
This commit is contained in:
Mike Hearn 2015-12-18 12:21:11 +01:00
parent 4cec5dac02
commit 6144ccc2c7
12 changed files with 60 additions and 52 deletions

View File

@ -66,8 +66,8 @@ class Cash : Contract {
}
// Just for grouping
interface Commands : Command {
class Move() : TypeOnlyCommand(), Commands
interface Commands : CommandData {
class Move() : TypeOnlyCommandData(), Commands
/**
* Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
@ -155,7 +155,7 @@ class Cash : Contract {
check(tx.inputStates().isEmpty())
check(tx.outputStates().sumCashOrNull() == null)
tx.addOutputState(Cash.State(at, amount, owner))
tx.addArg(WireCommand(Cash.Commands.Issue(), listOf(at.party.owningKey)))
tx.addCommand(Cash.Commands.Issue(), at.party.owningKey)
}
/**
@ -233,7 +233,7 @@ class Cash : Contract {
for (state in outputs) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code?
val keysList = keysUsed.toList()
tx.addArg(WireCommand(Commands.Move(), keysList))
tx.addCommand(Commands.Move(), keysList)
return keysList
}
}

View File

@ -53,12 +53,12 @@ class CommercialPaper : Contract {
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
}
interface Commands : Command {
class Move : TypeOnlyCommand(), Commands
class Redeem : TypeOnlyCommand(), Commands
interface Commands : CommandData {
class Move : TypeOnlyCommandData(), Commands
class Redeem : TypeOnlyCommandData(), Commands
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
class Issue : TypeOnlyCommand(), Commands
class Issue : TypeOnlyCommandData(), Commands
}
override fun verify(tx: TransactionForVerification) {
@ -119,7 +119,7 @@ class CommercialPaper : Contract {
*/
fun craftIssue(issuance: PartyReference, faceValue: Amount, maturityDate: Instant): PartialTransaction {
val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate)
return PartialTransaction().withItems(state, WireCommand(Commands.Issue(), issuance.party.owningKey))
return PartialTransaction().withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
}
/**
@ -128,7 +128,7 @@ class CommercialPaper : Contract {
fun craftMove(tx: PartialTransaction, paper: StateAndRef<State>, newOwner: PublicKey) {
tx.addInputState(paper.ref)
tx.addOutputState(paper.state.copy(owner = newOwner))
tx.addArg(WireCommand(Commands.Move(), paper.state.owner))
tx.addCommand(Commands.Move(), paper.state.owner)
}
/**
@ -143,7 +143,7 @@ class CommercialPaper : Contract {
// Add the cash movement using the states in our wallet.
Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet)
tx.addInputState(paper.ref)
tx.addArg(WireCommand(CommercialPaper.Commands.Redeem(), paper.state.owner))
tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner)
}
}

View File

@ -66,10 +66,10 @@ class CrowdFund : Contract {
)
interface Commands : Command {
class Register : TypeOnlyCommand(), Commands
class Pledge : TypeOnlyCommand(), Commands
class Close : TypeOnlyCommand(), Commands
interface Commands : CommandData {
class Register : TypeOnlyCommandData(), Commands
class Pledge : TypeOnlyCommandData(), Commands
class Close : TypeOnlyCommandData(), Commands
}
override fun verify(tx: TransactionForVerification) {
@ -146,7 +146,7 @@ class CrowdFund : Contract {
fun craftRegister(owner: PartyReference, fundingTarget: Amount, fundingName: String, closingTime: Instant): PartialTransaction {
val campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime)
val state = State(campaign)
return PartialTransaction().withItems(state, WireCommand(Commands.Register(), owner.party.owningKey))
return PartialTransaction().withItems(state, Command(Commands.Register(), owner.party.owningKey))
}
/**
@ -157,13 +157,13 @@ class CrowdFund : Contract {
tx.addOutputState(campaign.state.copy(
pledges = campaign.state.pledges + CrowdFund.Pledge(subscriber, 1000.DOLLARS)
))
tx.addArg(WireCommand(Commands.Pledge(), subscriber))
tx.addCommand(Commands.Pledge(), subscriber)
}
fun craftClose(tx: PartialTransaction, campaign: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
tx.addInputState(campaign.ref)
tx.addOutputState(campaign.state.copy(closed = true))
tx.addArg(WireCommand(Commands.Close(), campaign.state.campaign.owner))
tx.addCommand(Commands.Close(), campaign.state.campaign.owner)
// If campaign target has not been met, compose cash returns
if (campaign.state.pledgedAmount < campaign.state.campaign.target) {
for (pledge in campaign.state.pledges) {

View File

@ -91,7 +91,7 @@ public class JavaCommercialPaper implements Contract {
}
}
public static class Commands implements core.Command {
public static class Commands implements core.CommandData {
public static class Move extends Commands {
@Override
public boolean equals(Object obj) {
@ -114,7 +114,7 @@ public class JavaCommercialPaper implements Contract {
List<InOutGroup<State>> groups = tx.groupStates(State.class, State::withoutOwner);
// Find the command that instructs us what to do and check there's exactly one.
AuthenticatedObject<Command> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
Instant time = tx.getTime(); // Can be null/missing.

View File

@ -167,7 +167,7 @@ private class TwoPartyTradeProtocolImpl(private val smm: StateMachineManager) :
val freshKey = serviceHub.keyManagementService.freshKey()
val (command, state) = tradeRequest.assetForSale.state.withNewOwner(freshKey.public)
ptx.addOutputState(state)
ptx.addArg(WireCommand(command, tradeRequest.assetForSale.state.owner))
ptx.addCommand(command, tradeRequest.assetForSale.state.owner)
// Now sign the transaction with whatever keys we need to move the cash.
for (k in cashSigningPubKeys) {

View File

@ -106,17 +106,17 @@ fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) s
//// Authenticated commands ///////////////////////////////////////////////////////////////////////////////////////////
/** Filters the command list by type, party and public key all at once. */
inline fun <reified T : Command> List<AuthenticatedObject<Command>>.select(signer: PublicKey? = null, party: Party? = null) =
inline fun <reified T : CommandData> List<AuthenticatedObject<CommandData>>.select(signer: PublicKey? = null, party: Party? = null) =
filter { it.value is T }.
filter { if (signer == null) true else it.signers.contains(signer) }.
filter { if (party == null) true else it.signingParties.contains(party) }.
map { AuthenticatedObject<T>(it.signers, it.signingParties, it.value as T) }
inline fun <reified T : Command> List<AuthenticatedObject<Command>>.requireSingleCommand() = try {
inline fun <reified T : CommandData> List<AuthenticatedObject<CommandData>>.requireSingleCommand() = try {
select<T>().single()
} catch (e: NoSuchElementException) {
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
}
// For Java
fun List<AuthenticatedObject<Command>>.requireSingleCommand(klass: Class<out Command>) = filter { klass.isInstance(it) }.single()
fun List<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<out CommandData>) = filter { klass.isInstance(it) }.single()

View File

@ -31,7 +31,7 @@ interface OwnableState : ContractState {
val owner: PublicKey
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
fun withNewOwner(newOwner: PublicKey): Pair<Command, OwnableState>
fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState>
}
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
@ -63,14 +63,19 @@ data class PartyReference(val party: Party, val reference: OpaqueBytes) {
}
/** Marker interface for classes that represent commands */
interface Command
interface CommandData
/** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */
abstract class TypeOnlyCommand : Command {
abstract class TypeOnlyCommandData : CommandData {
override fun equals(other: Any?) = other?.javaClass == javaClass
override fun hashCode() = javaClass.name.hashCode()
}
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
data class Command(val data: CommandData, val pubkeys: List<PublicKey>) {
constructor(data: CommandData, key: PublicKey) : this(data, listOf(key))
}
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
data class AuthenticatedObject<out T : Any>(
val signers: List<PublicKey>,

View File

@ -46,19 +46,14 @@ import java.util.*
* database and replaced with the real objects. TFV is the form that is finally fed into the contracts.
*/
/** Serialized command plus pubkey pair: the signature is stored at the end of the serialized bytes */
data class WireCommand(val command: Command, val pubkeys: List<PublicKey>) {
constructor(command: Command, key: PublicKey) : this(command, listOf(key))
}
/** Transaction ready for serialisation, without any signatures attached. */
data class WireTransaction(val inputStates: List<ContractStateRef>,
val outputStates: List<ContractState>,
val commands: List<WireCommand>) {
val commands: List<Command>) {
fun toLedgerTransaction(timestamp: Instant?, identityService: IdentityService, originalHash: SecureHash): LedgerTransaction {
val authenticatedArgs = commands.map {
val institutions = it.pubkeys.mapNotNull { pk -> identityService.partyFromKey(pk) }
AuthenticatedObject(it.pubkeys, institutions, it.command)
AuthenticatedObject(it.pubkeys, institutions, it.data)
}
return LedgerTransaction(inputStates, outputStates, authenticatedArgs, timestamp, originalHash)
}
@ -67,14 +62,14 @@ data class WireTransaction(val inputStates: List<ContractStateRef>,
/** A mutable transaction that's in the process of being built, before all signatures are present. */
class PartialTransaction(private val inputStates: MutableList<ContractStateRef> = arrayListOf(),
private val outputStates: MutableList<ContractState> = arrayListOf(),
private val commands: MutableList<WireCommand> = arrayListOf()) {
private val commands: MutableList<Command> = arrayListOf()) {
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
public fun withItems(vararg items: Any): PartialTransaction {
for (t in items) {
when (t) {
is ContractStateRef -> inputStates.add(t)
is ContractState -> outputStates.add(t)
is WireCommand -> commands.add(t)
is Command -> commands.add(t)
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
}
}
@ -112,17 +107,20 @@ class PartialTransaction(private val inputStates: MutableList<ContractStateRef>
outputStates.add(state)
}
fun addArg(arg: WireCommand) {
fun addCommand(arg: Command) {
check(currentSigs.isEmpty())
// We should probably merge the lists of pubkeys for identical commands here.
commands.add(arg)
}
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
// Accessors that yield immutable snapshots.
fun inputStates(): List<ContractStateRef> = ArrayList(inputStates)
fun outputStates(): List<ContractState> = ArrayList(outputStates)
fun commands(): List<WireCommand> = ArrayList(commands)
fun commands(): List<Command> = ArrayList(commands)
}
data class SignedWireTransaction(val txBits: SerializedBytes<WireTransaction>, val sigs: List<DigitalSignature.WithKey>) {
@ -206,7 +204,7 @@ data class LedgerTransaction(
/** The states that will be generated by the execution of this transaction. */
val outStates: List<ContractState>,
/** Arbitrary data passed to the program of each input state. */
val commands: List<AuthenticatedObject<Command>>,
val commands: List<AuthenticatedObject<CommandData>>,
/** The moment the transaction was timestamped for, if a timestamp was present. */
val time: Instant?,
/** The hash of the original serialised TimestampedWireTransaction or SignedTransaction */
@ -227,7 +225,7 @@ data class LedgerTransaction(
/** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */
data class TransactionForVerification(val inStates: List<ContractState>,
val outStates: List<ContractState>,
val commands: List<AuthenticatedObject<Command>>,
val commands: List<AuthenticatedObject<CommandData>>,
val time: Instant?,
val origHash: SecureHash) {
override fun hashCode() = origHash.hashCode()

View File

@ -110,7 +110,7 @@ class CashTests {
assertEquals(100.DOLLARS, s.amount)
assertEquals(MINI_CORP, s.deposit.party)
assertEquals(DUMMY_PUBKEY_1, s.owner)
assertTrue(ptx.commands()[0].command is Cash.Commands.Issue)
assertTrue(ptx.commands()[0].data is Cash.Commands.Issue)
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].pubkeys[0])
}

View File

@ -31,7 +31,7 @@ class TransactionSerializationTests {
@Before
fun setup() {
tx = PartialTransaction().withItems(
fakeStateRef, outputState, changeState, WireCommand(Cash.Commands.Move(), arrayListOf(TestUtils.keypair.public))
fakeStateRef, outputState, changeState, Command(Cash.Commands.Move(), arrayListOf(TestUtils.keypair.public))
)
}
@ -77,7 +77,7 @@ class TransactionSerializationTests {
// If the signature was replaced in transit, we don't like it.
assertFailsWith(SignatureException::class) {
val tx2 = PartialTransaction().withItems(fakeStateRef, outputState, changeState,
WireCommand(Cash.Commands.Move(), arrayListOf(TestUtils.keypair2.public)))
Command(Cash.Commands.Move(), TestUtils.keypair2.public))
tx2.signWith(TestUtils.keypair2)
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
@ -89,7 +89,7 @@ class TransactionSerializationTests {
tx.signWith(TestUtils.keypair)
val ttx = tx.toSignedTransaction().toTimestampedTransactionWithoutTime()
val ltx = ttx.verifyToLedgerTransaction(DUMMY_TIMESTAMPER, MockIdentityService)
assertEquals(tx.commands().map { it.command }, ltx.commands.map { it.value })
assertEquals(tx.commands().map { it.data }, ltx.commands.map { it.value })
assertEquals(tx.inputStates(), ltx.inStateRefs)
assertEquals(tx.outputStates(), ltx.outStates)
assertNull(ltx.time)

View File

@ -176,13 +176,17 @@ infix fun ContractState.label(label: String) = LabeledOutput(label, this)
abstract class AbstractTransactionForTest {
protected val outStates = ArrayList<LabeledOutput>()
protected val commands = ArrayList<AuthenticatedObject<Command>>()
protected val commands = ArrayList<Command>()
open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) }
fun arg(vararg key: PublicKey, c: () -> Command) {
protected fun commandsToAuthenticatedObjects(): List<AuthenticatedObject<CommandData>> {
return commands.map { AuthenticatedObject(it.pubkeys, it.pubkeys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, it.data) }
}
fun arg(vararg key: PublicKey, c: () -> CommandData) {
val keys = listOf(*key)
commands.add(AuthenticatedObject(keys, keys.mapNotNull { TEST_KEYS_TO_CORP_MAP[it] }, c()))
commands.add(Command(c(), keys))
}
// Forbid patterns like: transaction { ... transaction { ... } }
@ -196,7 +200,8 @@ open class TransactionForTest : AbstractTransactionForTest() {
fun input(s: () -> ContractState) = inStates.add(s())
protected fun run(time: Instant) {
val tx = TransactionForVerification(inStates, outStates.map { it.state }, commands, time, SecureHash.randomSHA256())
val tx = TransactionForVerification(inStates, outStates.map { it.state }, commandsToAuthenticatedObjects(),
time, SecureHash.randomSHA256())
tx.verify(TEST_PROGRAM_MAP)
}
@ -280,8 +285,8 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
* hash (i.e. pretend it was signed)
*/
fun toLedgerTransaction(time: Instant): LedgerTransaction {
val wireCmds = commands.map { WireCommand(it.value, it.signers) }
return WireTransaction(inStates, outStates.map { it.state }, wireCmds).toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
val wtx = WireTransaction(inStates, outStates.map { it.state }, commands)
return wtx.toLedgerTransaction(time, MockIdentityService, SecureHash.randomSHA256())
}
}

View File

@ -8,7 +8,7 @@
package core.visualiser
import core.Command
import core.CommandData
import core.ContractState
import core.SecureHash
import core.testutils.TransactionGroupDSL
@ -67,7 +67,7 @@ class GraphVisualiser(val dsl: TransactionGroupDSL<in ContractState>) {
return dsl.labelForState(state) ?: stateToTypeName(state)
}
private fun commandToTypeName(state: Command) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.')
private fun commandToTypeName(state: CommandData) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.')
private fun stateToTypeName(state: ContractState) = state.javaClass.canonicalName.removePrefix("contracts.").removeSuffix(".State")
private fun stateToCSSClass(state: ContractState) = stateToTypeName(state).replace('.', '_').toLowerCase()