Platform: commands can now have multiple signatures per command (i.e. you only have one command of any type per transaction even if there are multiple authorisations).

This commit is contained in:
Mike Hearn
2015-11-09 19:27:53 +00:00
parent 0dc92822b5
commit ac9a371179
8 changed files with 33 additions and 24 deletions

View File

@ -22,6 +22,7 @@ buildscript {
dependencies { dependencies {
testCompile 'junit:junit:4.11' testCompile 'junit:junit:4.11'
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile "com.google.guava:guava:18.0" compile "com.google.guava:guava:18.0"
compile "org.funktionale:funktionale:0.6_1.0.0-beta" compile "org.funktionale:funktionale:0.6_1.0.0-beta"
} }

View File

@ -105,7 +105,7 @@ object Cash : Contract {
// see a signature from each of those keys. The actual signatures have been verified against the transaction // see a signature from each of those keys. The actual signatures have been verified against the transaction
// data by the platform before execution. // data by the platform before execution.
val owningPubKeys = cashInputs.map { it.owner }.toSortedSet() val owningPubKeys = cashInputs.map { it.owner }.toSortedSet()
val keysThatSigned = args.select<Commands.Move>().map { it.signer }.toSortedSet() val keysThatSigned = args.requireSingleCommand<Commands.Move>().signers.toSortedSet()
requireThat { requireThat {
"the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned) "the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned)
} }
@ -177,7 +177,7 @@ object Cash : Contract {
} else states } else states
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet. // Finally, generate the commands. Pretend to sign here, real signatures aren't done yet.
val commands = keysUsed.map { VerifiedSigned(it, null, Commands.Move()) } val commands = keysUsed.map { VerifiedSigned(listOf(it), emptyList(), Commands.Move()) }
return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList()) return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
} }

View File

@ -53,14 +53,14 @@ object ComedyPaper : Contract {
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.signers.contains(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.signers.contains(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()

View File

@ -18,11 +18,16 @@ import kotlin.math.div
// region Misc // region Misc
inline fun <reified T : Command> List<VerifiedSigned<Command>>.select(signer: PublicKey? = null, institution: Institution? = null) = inline fun <reified T : Command> List<VerifiedSigned<Command>>.select(signer: PublicKey? = null, institution: Institution? = null) =
filter { it.value is T }. filter { it.value is T }.
filter { if (signer == null) true else signer == it.signer }. filter { if (signer == null) true else it.signers.contains(signer) }.
filter { if (institution == null) true else institution == it.signingInstitution }. filter { if (institution == null) true else it.signingInstitutions.contains(institution) }.
map { VerifiedSigned<T>(it.signer, it.signingInstitution, it.value as T) } map { VerifiedSigned<T>(it.signers, it.signingInstitutions, it.value as T) }
inline fun <reified T : Command> List<VerifiedSigned<Command>>.requireSingleCommand() = select<T>().single() inline fun <reified T : Command> List<VerifiedSigned<Command>>.requireSingleCommand() = try {
select<T>().single()
} catch (e: NoSuchElementException) {
// Better error message.
throw IllegalStateException("Required ${T::class.simpleName} command")
}
// endregion // endregion

View File

@ -49,22 +49,20 @@ interface Command
/** Provided as an input to a contract; converted to a [VerifiedSignedCommand] by the platform before execution. */ /** Provided as an input to a contract; converted to a [VerifiedSignedCommand] by the platform before execution. */
data class SignedCommand( data class SignedCommand(
/** Signature over this object to prove who it came from */ /** Signatures over this object to prove who it came from: this is fetched off the end of the transaction wire format. */
val commandDataSignature: DigitalSignature.WithKey, val commandDataSignatures: List<DigitalSignature.WithKey>,
/** Command data, deserialized to an implementation of [Command] */ /** Command data, deserialized to an implementation of [Command] */
val serialized: OpaqueBytes, val serialized: OpaqueBytes,
/** Identifies what command the serialized data contains (should maybe be a hash too) */ /** Identifies what command the serialized data contains (hash of bytecode?) */
val classID: String, val classID: SecureHash
/** Hash of a derivative of the transaction data, so this command can only ever apply to one transaction */
val txBindingHash: SecureHash.SHA256
) )
/** Obtained from a [SignedCommand], deserialised and signature checked */ /** Obtained from a [SignedCommand], deserialised and signature checked */
data class VerifiedSigned<out T : Command>( data class VerifiedSigned<out T : Command>(
val signer: PublicKey, val signers: List<PublicKey>,
/** If the public key was recognised, the looked up institution is available here, otherwise it's null */ /** If the public key was recognised, the looked up institution is available here, otherwise it's null */
val signingInstitution: Institution?, val signingInstitutions: List<Institution>,
val value: T val value: T
) )

View File

@ -67,7 +67,7 @@ data class TransactionForTest(
) { ) {
fun input(s: () -> ContractState) = inStates.add(s()) fun input(s: () -> ContractState) = inStates.add(s())
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(listOf(key), TEST_KEYS_TO_CORP_MAP[key].let { if (it != null) listOf(it) else emptyList() }, c()))
private fun run() = TransactionForVerification(inStates, outStates, args, TEST_TX_TIME).verify(TEST_PROGRAM_MAP) private fun run() = TransactionForVerification(inStates, outStates, args, TEST_TX_TIME).verify(TEST_PROGRAM_MAP)
@ -79,7 +79,7 @@ data class TransactionForTest(
if (m == null) if (m == null)
fail("Threw exception without a message") fail("Threw exception without a message")
else else
if (!m.contains(msg)) throw AssertionError("Error was actually: $m", e) if (!m.toLowerCase().contains(msg.toLowerCase())) throw AssertionError("Error was actually: $m", e)
} }
} }

View File

@ -7,13 +7,17 @@ import java.time.Instant
// WireTransaction -> LedgerTransaction -> TransactionForVerification // WireTransaction -> LedgerTransaction -> TransactionForVerification
// TransactionForTest // TransactionForTest
class WireTransaction { class WireTransaction(
// TODO: This is supposed to be a protocol buffer, FIX SPE message, etc. For prototype it can just be Kryo serialised // TODO: This is supposed to be a protocol buffer, FIX SPE message, etc. For prototype it can just be Kryo serialised.
} val tx: ByteArray,
// We assume Ed25519 signatures for all. Num signatures == array.length / 64 (each sig is 64 bytes in size)
val signatures: ByteArray
)
/** /**
* A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states. * 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 * It is the first step after extraction from a WireTransaction. The signature part is tricky.
*/ */
class LedgerTransaction( class LedgerTransaction(
/** The input states which will be consumed/invalidated by the execution of this transaction. */ /** The input states which will be consumed/invalidated by the execution of this transaction. */

View File

@ -34,7 +34,7 @@ class CashTests {
transaction { transaction {
output { outState } output { outState }
// No command arguments // No command arguments
this `fails requirement` "the owning keys are the same as the signing keys" this `fails requirement` "required move command"
} }
transaction { transaction {
output { outState } output { outState }
@ -161,12 +161,13 @@ class CashTests {
transaction { transaction {
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(100.DOLLARS) } arg(MEGA_CORP_KEY) { Cash.Commands.Exit(100.DOLLARS) }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `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) }
this `fails requirement` "the owning keys are the same as the signing keys" // No move command. this `fails requirement` "required move command"
transaction { transaction {
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }