From ac9a371179a27d49e0cfd8ce6b149e597ded990c Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 9 Nov 2015 19:27:53 +0000 Subject: [PATCH] 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). --- build.gradle | 1 + src/contracts/Cash.kt | 4 ++-- src/contracts/ComedyPaper.kt | 4 ++-- src/core/ContractsDSL.kt | 13 +++++++++---- src/core/Structures.kt | 14 ++++++-------- src/core/TestUtils.kt | 4 ++-- src/core/Transactions.kt | 12 ++++++++---- tests/contracts/CashTests.kt | 5 +++-- 8 files changed, 33 insertions(+), 24 deletions(-) diff --git a/build.gradle b/build.gradle index f70d25e88b..a71a72bc9b 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ buildscript { dependencies { testCompile 'junit:junit:4.11' compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "com.google.guava:guava:18.0" compile "org.funktionale:funktionale:0.6_1.0.0-beta" } diff --git a/src/contracts/Cash.kt b/src/contracts/Cash.kt index fa7bfe850d..47f196bb18 100644 --- a/src/contracts/Cash.kt +++ b/src/contracts/Cash.kt @@ -105,7 +105,7 @@ object Cash : Contract { // 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().map { it.signer }.toSortedSet() + val keysThatSigned = args.requireSingleCommand().signers.toSortedSet() requireThat { "the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned) } @@ -177,7 +177,7 @@ object Cash : Contract { } else states // 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()) } diff --git a/src/contracts/ComedyPaper.kt b/src/contracts/ComedyPaper.kt index 29a6be3f45..03dae9825b 100644 --- a/src/contracts/ComedyPaper.kt +++ b/src/contracts/ComedyPaper.kt @@ -53,14 +53,14 @@ object ComedyPaper : Contract { when (command.value) { is Commands.Move -> requireThat { val output = outStates.filterIsInstance().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()) } is Commands.Redeem -> requireThat { val received = outStates.sumCash() // 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 received amount equals the face value" by (received == input.faceValue) "the paper must be destroyed" by outStates.filterIsInstance().none() diff --git a/src/core/ContractsDSL.kt b/src/core/ContractsDSL.kt index 4d91a21162..3479b8804d 100644 --- a/src/core/ContractsDSL.kt +++ b/src/core/ContractsDSL.kt @@ -18,11 +18,16 @@ import kotlin.math.div // region Misc inline fun List>.select(signer: PublicKey? = null, institution: Institution? = null) = filter { it.value is T }. - filter { if (signer == null) true else signer == it.signer }. - filter { if (institution == null) true else institution == it.signingInstitution }. - map { VerifiedSigned(it.signer, it.signingInstitution, it.value as T) } + filter { if (signer == null) true else it.signers.contains(signer) }. + filter { if (institution == null) true else it.signingInstitutions.contains(institution) }. + map { VerifiedSigned(it.signers, it.signingInstitutions, it.value as T) } -inline fun List>.requireSingleCommand() = select().single() +inline fun List>.requireSingleCommand() = try { + select().single() +} catch (e: NoSuchElementException) { + // Better error message. + throw IllegalStateException("Required ${T::class.simpleName} command") +} // endregion diff --git a/src/core/Structures.kt b/src/core/Structures.kt index 561b5f9ffa..92f0759545 100644 --- a/src/core/Structures.kt +++ b/src/core/Structures.kt @@ -49,22 +49,20 @@ interface Command /** Provided as an input to a contract; converted to a [VerifiedSignedCommand] by the platform before execution. */ data class SignedCommand( - /** Signature over this object to prove who it came from */ - val commandDataSignature: DigitalSignature.WithKey, + /** Signatures over this object to prove who it came from: this is fetched off the end of the transaction wire format. */ + val commandDataSignatures: List, /** Command data, deserialized to an implementation of [Command] */ val serialized: OpaqueBytes, - /** Identifies what command the serialized data contains (should maybe be a hash too) */ - val classID: String, - /** Hash of a derivative of the transaction data, so this command can only ever apply to one transaction */ - val txBindingHash: SecureHash.SHA256 + /** Identifies what command the serialized data contains (hash of bytecode?) */ + val classID: SecureHash ) /** Obtained from a [SignedCommand], deserialised and signature checked */ data class VerifiedSigned( - val signer: PublicKey, + val signers: List, /** If the public key was recognised, the looked up institution is available here, otherwise it's null */ - val signingInstitution: Institution?, + val signingInstitutions: List, val value: T ) diff --git a/src/core/TestUtils.kt b/src/core/TestUtils.kt index 6171fe3a74..3bdf3f09ae 100644 --- a/src/core/TestUtils.kt +++ b/src/core/TestUtils.kt @@ -67,7 +67,7 @@ data class TransactionForTest( ) { fun input(s: () -> ContractState) = inStates.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) @@ -79,7 +79,7 @@ data class TransactionForTest( if (m == null) fail("Threw exception without a message") 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) } } diff --git a/src/core/Transactions.kt b/src/core/Transactions.kt index fffe0edc6f..a5ac7c91a6 100644 --- a/src/core/Transactions.kt +++ b/src/core/Transactions.kt @@ -7,13 +7,17 @@ import java.time.Instant // 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 -} +class WireTransaction( + // 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. - * It is the first step after extraction + * It is the first step after extraction from a WireTransaction. The signature part is tricky. */ class LedgerTransaction( /** The input states which will be consumed/invalidated by the execution of this transaction. */ diff --git a/tests/contracts/CashTests.kt b/tests/contracts/CashTests.kt index a21049128e..a78d6e28d3 100644 --- a/tests/contracts/CashTests.kt +++ b/tests/contracts/CashTests.kt @@ -34,7 +34,7 @@ class CashTests { transaction { output { outState } // No command arguments - this `fails requirement` "the owning keys are the same as the signing keys" + this `fails requirement` "required move command" } transaction { output { outState } @@ -161,12 +161,13 @@ class CashTests { transaction { arg(MEGA_CORP_KEY) { Cash.Commands.Exit(100.DOLLARS) } + arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } this `fails requirement` "the amounts balance" } transaction { 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 { arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }