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 {
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"
}

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
// data by the platform before execution.
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 {
"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())
}

View File

@ -53,14 +53,14 @@ object ComedyPaper : Contract {
when (command.value) {
is Commands.Move -> requireThat {
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())
}
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<ComedyPaper.State>().none()

View File

@ -18,11 +18,16 @@ import kotlin.math.div
// region Misc
inline fun <reified T : Command> List<VerifiedSigned<Command>>.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<T>(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<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

View File

@ -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<DigitalSignature.WithKey>,
/** 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<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 */
val signingInstitution: Institution?,
val signingInstitutions: List<Institution>,
val value: T
)

View File

@ -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)
}
}

View File

@ -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. */

View File

@ -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() }