Introduce a PartialTransaction class to represent a mutable transaction that we're in the process of building/signing.

This commit is contained in:
Mike Hearn 2015-11-16 20:07:52 +01:00
parent 853b37a6e1
commit 1f17053263
3 changed files with 92 additions and 32 deletions

View File

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

View File

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

View File

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