mirror of
https://github.com/corda/corda.git
synced 2025-02-21 01:42:24 +00:00
Introduce a PartialTransaction class to represent a mutable transaction that we're in the process of building/signing.
This commit is contained in:
parent
853b37a6e1
commit
1f17053263
@ -53,14 +53,16 @@ object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||
}
|
||||
|
||||
/** Utility to simplify the act of signing a byte array */
|
||||
fun PrivateKey.signWithECDSA(bits: ByteArray, publicKey: PublicKey? = null): DigitalSignature {
|
||||
fun PrivateKey.signWithECDSA(bits: ByteArray): DigitalSignature {
|
||||
val signer = Signature.getInstance("SHA256withECDSA")
|
||||
signer.initSign(this)
|
||||
signer.update(bits)
|
||||
val sig = signer.sign()
|
||||
return if (publicKey == null) DigitalSignature(sig) else DigitalSignature.WithKey(publicKey, sig)
|
||||
return DigitalSignature(sig)
|
||||
}
|
||||
|
||||
fun PrivateKey.signWithECDSA(bits: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, signWithECDSA(bits).bits)
|
||||
|
||||
/** Utility to simplify the act of verifying a signature */
|
||||
fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) {
|
||||
val verifier = Signature.getInstance("SHA256withECDSA")
|
||||
|
@ -24,6 +24,11 @@ import java.util.*
|
||||
* to a timestamping authority does NOT change its hash (this may be an issue that leads to confusion and should be
|
||||
* examined more closely).
|
||||
*
|
||||
* A PartialTransaction is a transaction class that's mutable (unlike the others which are all immutable). It is
|
||||
* intended to be passed around contracts that may edit it by adding new states/commands or modifying the existing set.
|
||||
* Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from
|
||||
* multiple parties.
|
||||
*
|
||||
* LedgerTransaction is derived from WireTransaction and TimestampedWireTransaction together. It is the result of
|
||||
* doing some basic key lookups on WireCommand to see if any keys are from a recognised institution, thus converting
|
||||
* the WireCommand objects into AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->institution
|
||||
@ -42,20 +47,6 @@ data class WireCommand(val command: Command, val pubkeys: List<PublicKey>) : Ser
|
||||
data class WireTransaction(val inputStates: List<ContractStateRef>,
|
||||
val outputStates: List<ContractState>,
|
||||
val args: List<WireCommand>) : SerializeableWithKryo {
|
||||
fun signWith(keys: List<KeyPair>): SignedWireTransaction {
|
||||
val keyMap = keys.map { it.public to it.private }.toMap()
|
||||
val bits = serialize()
|
||||
val signWith = args.flatMap { it.pubkeys }.toSet()
|
||||
if (keys.size != signWith.size)
|
||||
throw IllegalArgumentException("Incorrect number of keys provided: ${keys.size} vs ${signWith.size}")
|
||||
val sigs = ArrayList<DigitalSignature.WithKey>()
|
||||
for (key in signWith) {
|
||||
val priv = keyMap[key] ?: throw IllegalArgumentException("Command without private signing key found")
|
||||
sigs.add(priv.signWithECDSA(bits, key) as DigitalSignature.WithKey)
|
||||
}
|
||||
return SignedWireTransaction(bits, sigs)
|
||||
}
|
||||
|
||||
val hash: SecureHash get() = SecureHash.sha256(serialize())
|
||||
|
||||
fun toLedgerTransaction(timestamp: Instant, institutionKeyMap: Map<PublicKey, Institution>): LedgerTransaction {
|
||||
@ -67,6 +58,59 @@ data class WireTransaction(val inputStates: List<ContractStateRef>,
|
||||
}
|
||||
}
|
||||
|
||||
/** A mutable transaction that's in the process of being built, before all signatures are present. */
|
||||
class PartialTransaction(private val inputStates: MutableList<ContractStateRef> = arrayListOf(),
|
||||
private val outputStates: MutableList<ContractState> = arrayListOf(),
|
||||
private val args: MutableList<WireCommand> = arrayListOf()) {
|
||||
|
||||
/** A more convenient constructor that sorts things into the right lists for you */
|
||||
constructor(vararg things: Any) : this() {
|
||||
for (t in things) {
|
||||
when (t) {
|
||||
is ContractStateRef -> inputStates.add(t)
|
||||
is ContractState -> outputStates.add(t)
|
||||
is WireCommand -> args.add(t)
|
||||
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The signatures that have been collected so far - might be incomplete! */
|
||||
private val currentSigs = arrayListOf<DigitalSignature.WithKey>()
|
||||
|
||||
fun signWith(key: KeyPair) {
|
||||
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
|
||||
check(args.count { it.pubkeys.contains(key.public) } > 0) { "Trying to sign with a key that isn't in any command" }
|
||||
val bits = toWire().serialize()
|
||||
currentSigs.add(key.private.signWithECDSA(bits, key.public))
|
||||
}
|
||||
|
||||
private fun toWire() = WireTransaction(inputStates, outputStates, args)
|
||||
|
||||
fun toSignedTransaction(): SignedWireTransaction {
|
||||
val requiredKeys = args.flatMap { it.pubkeys }.toSet()
|
||||
val gotKeys = currentSigs.map { it.by }.toSet()
|
||||
check(gotKeys == requiredKeys) { "The set of required signatures isn't equal to the signatures we've got" }
|
||||
return SignedWireTransaction(toWire().serialize(), ArrayList(currentSigs))
|
||||
}
|
||||
|
||||
fun addInputState(ref: ContractStateRef) {
|
||||
check(currentSigs.isEmpty())
|
||||
inputStates.add(ref)
|
||||
}
|
||||
|
||||
fun addOutputState(state: ContractState) {
|
||||
check(currentSigs.isEmpty())
|
||||
outputStates.add(state)
|
||||
}
|
||||
|
||||
fun addArg(arg: WireCommand) {
|
||||
check(currentSigs.isEmpty())
|
||||
args.add(arg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class SignedWireTransaction(val txBits: ByteArray, val sigs: List<DigitalSignature.WithKey>) : SerializeableWithKryo {
|
||||
init {
|
||||
check(sigs.isNotEmpty())
|
||||
|
@ -2,6 +2,7 @@ package serialization
|
||||
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import testutils.TestUtils
|
||||
import java.security.SignatureException
|
||||
@ -15,15 +16,19 @@ class TransactionSerializationTests {
|
||||
val changeState = Cash.State(depositRef, 400.POUNDS, TestUtils.keypair.public)
|
||||
|
||||
val fakeStateRef = ContractStateRef(SecureHash.sha256("fake tx id"), 0)
|
||||
val tx = WireTransaction(
|
||||
arrayListOf(fakeStateRef),
|
||||
arrayListOf(outputState, changeState),
|
||||
arrayListOf(WireCommand(Cash.Commands.Move, arrayListOf(TestUtils.keypair.public)))
|
||||
)
|
||||
lateinit var tx: PartialTransaction
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
tx = PartialTransaction(
|
||||
fakeStateRef, outputState, changeState, WireCommand(Cash.Commands.Move, arrayListOf(TestUtils.keypair.public))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signWireTX() {
|
||||
val signedTX: SignedWireTransaction = tx.signWith(listOf(TestUtils.keypair))
|
||||
tx.signWith(TestUtils.keypair)
|
||||
val signedTX = tx.toSignedTransaction()
|
||||
|
||||
// Now check that the signature we just made verifies.
|
||||
signedTX.verify()
|
||||
@ -36,17 +41,23 @@ class TransactionSerializationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrongKeys() {
|
||||
// Can't sign without the private key.
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
tx.signWith(listOf())
|
||||
fun tooManyKeys() {
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
tx.signWith(TestUtils.keypair)
|
||||
tx.signWith(TestUtils.keypair2)
|
||||
tx.toSignedTransaction()
|
||||
}
|
||||
// Can't sign with too many keys.
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
tx.signWith(listOf(TestUtils.keypair, TestUtils.keypair2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrongKeys() {
|
||||
// Can't convert if we don't have enough signatures.
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
tx.toSignedTransaction()
|
||||
}
|
||||
|
||||
val signedTX = tx.signWith(listOf(TestUtils.keypair))
|
||||
tx.signWith(TestUtils.keypair)
|
||||
val signedTX = tx.toSignedTransaction()
|
||||
|
||||
// Cannot construct with an empty sigs list.
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
@ -55,8 +66,11 @@ class TransactionSerializationTests {
|
||||
|
||||
// If the signature was replaced in transit, we don't like it.
|
||||
assertFailsWith(SignatureException::class) {
|
||||
val tx2 = tx.copy(args = listOf(WireCommand(Cash.Commands.Move, arrayListOf(TestUtils.keypair2.public)))).signWith(listOf(TestUtils.keypair2))
|
||||
signedTX.copy(sigs = tx2.sigs).verify()
|
||||
val tx2 = PartialTransaction(fakeStateRef, outputState, changeState,
|
||||
WireCommand(Cash.Commands.Move, arrayListOf(TestUtils.keypair2.public)))
|
||||
tx2.signWith(TestUtils.keypair2)
|
||||
|
||||
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user