mirror of
https://github.com/corda/corda.git
synced 2025-04-07 19:34:41 +00:00
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:
parent
0dc92822b5
commit
ac9a371179
@ -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"
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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. */
|
||||
|
@ -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() }
|
||||
|
Loading…
x
Reference in New Issue
Block a user