mirror of
https://github.com/corda/corda.git
synced 2025-06-18 15:18:16 +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:
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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. */
|
||||||
|
@ -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() }
|
||||||
|
Reference in New Issue
Block a user