diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionGraphSearch.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionGraphSearch.kt index 98fe1bd369..404dda5a3e 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionGraphSearch.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionGraphSearch.kt @@ -45,7 +45,7 @@ class TransactionGraphSearch(val transactions: ReadOnlyTransactionStorage, val unvisitedInputTxs: Map = inputsLeadingToUnvisitedTx.map { it.txhash }.toHashSet().map { transactions.getTransaction(it) }.filterNotNull().associateBy { it.id } val unvisitedInputTxsWithInputIndex: Iterable> = inputsLeadingToUnvisitedTx.filter { it.txhash in unvisitedInputTxs.keys }.map { Pair(unvisitedInputTxs[it.txhash]!!, it.index) } next += (unvisitedInputTxsWithInputIndex.filter { q.followInputsOfType == null || it.first.tx.outputs[it.second].data.javaClass == q.followInputsOfType } - .map { it.first }.filter { alreadyVisited.add(it.txBits.hash) }.map { it.tx }) + .map { it.first }.filter { alreadyVisited.add(it.id) }.map { it.tx }) } return results diff --git a/core/src/main/kotlin/com/r3corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/com/r3corda/core/transactions/SignedTransaction.kt index 10040cd6fd..4ed2ed4fcb 100644 --- a/core/src/main/kotlin/com/r3corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/com/r3corda/core/transactions/SignedTransaction.kt @@ -19,9 +19,12 @@ import java.util.* * of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different STs may * map to the same key (and they could be different in important ways, like validity!). The signatures on a * SignedTransaction might be invalid or missing: the type does not imply validity. + * A transaction ID should be the hash of the [WireTransaction] Merkle tree root. Thus adding or removing a signature does not change it. */ data class SignedTransaction(val txBits: SerializedBytes, - val sigs: List) : NamedByHash { + val sigs: List, + override val id: SecureHash +) : NamedByHash { init { require(sigs.isNotEmpty()) } @@ -31,9 +34,6 @@ data class SignedTransaction(val txBits: SerializedBytes, /** Lazily calculated access to the deserialised/hashed transaction data. */ val tx: WireTransaction by lazy { WireTransaction.deserialize(txBits) } - /** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */ - override val id: SecureHash get() = tx.id - class SignaturesMissingException(val missing: Set, val descriptions: List, override val id: SecureHash) : NamedByHash, SignatureException() { override fun toString(): String { return "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.toStringsShort()}" @@ -64,6 +64,7 @@ data class SignedTransaction(val txBits: SerializedBytes, if (needed.isNotEmpty()) throw SignaturesMissingException(needed, getMissingKeyDescriptions(needed), id) } + check(tx.id == id) return tx } @@ -77,8 +78,9 @@ data class SignedTransaction(val txBits: SerializedBytes, */ @Throws(SignatureException::class) fun checkSignaturesAreValid() { - for (sig in sigs) - sig.verifyWithECDSA(txBits.bits) + for (sig in sigs) { + sig.verifyWithECDSA(id.bits) + } } private fun getMissingSignatures(): Set { diff --git a/core/src/main/kotlin/com/r3corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/com/r3corda/core/transactions/TransactionBuilder.kt index 34d5e0f623..c6aefc7689 100644 --- a/core/src/main/kotlin/com/r3corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/transactions/TransactionBuilder.kt @@ -99,7 +99,7 @@ open class TransactionBuilder( fun signWith(key: KeyPair): TransactionBuilder { check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" } - val data = toWireTransaction().serialize() + val data = toWireTransaction().id addSignatureUnchecked(key.signWithECDSA(data.bits)) return this } @@ -124,7 +124,7 @@ open class TransactionBuilder( */ fun checkSignature(sig: DigitalSignature.WithKey) { require(commands.any { it.signers.contains(sig.by) }) { "Signature key doesn't match any command" } - sig.verifyWithECDSA(toWireTransaction().serialized) + sig.verifyWithECDSA(toWireTransaction().id) } /** Adds the signature directly to the transaction, without checking it for validity. */ @@ -143,7 +143,8 @@ open class TransactionBuilder( if (missing.isNotEmpty()) throw IllegalStateException("Missing signatures on the transaction for the public keys: ${missing.toStringsShort()}") } - return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs)) + val wtx = toWireTransaction() + return SignedTransaction(wtx.serialize(), ArrayList(currentSigs), wtx.id) } open fun addInputState(stateAndRef: StateAndRef<*>) { diff --git a/core/src/main/kotlin/com/r3corda/protocols/AbstractStateReplacementProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/AbstractStateReplacementProtocol.kt index c702791765..ff3cc2f914 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/AbstractStateReplacementProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/AbstractStateReplacementProtocol.kt @@ -96,7 +96,7 @@ abstract class AbstractStateReplacementProtocol { if (it.sig == null) throw StateReplacementException(it.error!!) else { check(it.sig.by == party.owningKey) { "Not signed by the required participant" } - it.sig.verifyWithECDSA(stx.txBits) + it.sig.verifyWithECDSA(stx.id) it.sig } } @@ -154,7 +154,7 @@ abstract class AbstractStateReplacementProtocol { // TODO: This step should not be necessary, as signatures are re-checked in verifySignatures. val allSignatures = swapSignatures.unwrap { signatures -> - signatures.forEach { it.verifyWithECDSA(stx.txBits) } + signatures.forEach { it.verifyWithECDSA(stx.id) } signatures } @@ -199,7 +199,7 @@ abstract class AbstractStateReplacementProtocol { private fun sign(stx: SignedTransaction): DigitalSignature.WithKey { val myKey = serviceHub.legalIdentityKey - return myKey.signWithECDSA(stx.txBits) + return myKey.signWithECDSA(stx.id) } } diff --git a/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt index 528decd434..109391fa29 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/NotaryProtocol.kt @@ -66,7 +66,7 @@ object NotaryProtocol { progressTracker.currentStep = VALIDATING when (notaryResult) { is Result.Success -> { - validateSignature(notaryResult.sig, stx.txBits) + validateSignature(notaryResult.sig, stx.id.bits) notaryResult.sig } is Result.Error -> { @@ -79,7 +79,7 @@ object NotaryProtocol { } } - private fun validateSignature(sig: DigitalSignature.LegallyIdentifiable, data: SerializedBytes) { + private fun validateSignature(sig: DigitalSignature.LegallyIdentifiable, data: ByteArray) { check(sig.signer == notaryParty) { "Notary result not signed by the correct service" } sig.verifyWithECDSA(data) } @@ -108,7 +108,7 @@ object NotaryProtocol { beforeCommit(stx, reqIdentity) commitInputStates(wtx, reqIdentity) - val sig = sign(stx.txBits) + val sig = sign(stx.id.bits) Result.Success(sig) } catch(e: NotaryException) { Result.Error(e.error) @@ -140,12 +140,12 @@ object NotaryProtocol { uniquenessProvider.commit(tx.inputs, tx.id, reqIdentity) } catch (e: UniquenessException) { val conflictData = e.error.serialize() - val signedConflict = SignedData(conflictData, sign(conflictData)) + val signedConflict = SignedData(conflictData, sign(conflictData.bits)) throw NotaryException(NotaryError.Conflict(tx, signedConflict)) } } - private fun sign(bits: SerializedBytes): DigitalSignature.LegallyIdentifiable { + private fun sign(bits: ByteArray): DigitalSignature.LegallyIdentifiable { val myNodeInfo = serviceHub.myInfo val myIdentity = myNodeInfo.notaryIdentity val mySigningKey = serviceHub.notaryIdentityKey diff --git a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt index 8f8d32c388..7dd805fdd7 100644 --- a/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt +++ b/core/src/main/kotlin/com/r3corda/protocols/TwoPartyDealProtocol.kt @@ -170,7 +170,7 @@ object TwoPartyDealProtocol { open fun computeOurSignature(partialTX: SignedTransaction): DigitalSignature.WithKey { progressTracker.currentStep = SIGNING - return myKeyPair.signWithECDSA(partialTX.txBits) + return myKeyPair.signWithECDSA(partialTX.id) } @Suspendable diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTests.kt index 0e7b718d95..c5e4a3216e 100644 --- a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionTests.kt @@ -34,7 +34,7 @@ class TransactionTests { timestamp = null ) val bits: SerializedBytes = wtx.serialized - fun make(vararg keys: KeyPair) = SignedTransaction(bits, keys.map { it.signWithECDSA(bits) }) + fun make(vararg keys: KeyPair) = SignedTransaction(bits, keys.map { it.signWithECDSA(wtx.id.bits) }, wtx.id) assertFailsWith { make().verifySignatures() } assertEquals( diff --git a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt index e29fb02903..ea5b18e84b 100644 --- a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt @@ -67,7 +67,7 @@ class TransactionSerializationTests { signedTX.verifySignatures() // Corrupt the data and ensure the signature catches the problem. - signedTX.txBits.bits[5] = 0 + signedTX.id.bits[5] = 0 assertFailsWith(SignatureException::class) { signedTX.verifySignatures() } diff --git a/finance/src/main/kotlin/com/r3corda/contracts/testing/Generators.kt b/finance/src/main/kotlin/com/r3corda/contracts/testing/Generators.kt index 765d614ffd..5175e8884c 100644 --- a/finance/src/main/kotlin/com/r3corda/contracts/testing/Generators.kt +++ b/finance/src/main/kotlin/com/r3corda/contracts/testing/Generators.kt @@ -81,7 +81,8 @@ class SignedTransactionGenerator: Generator(SignedTransaction val wireTransaction = WiredTransactionGenerator().generate(random, status) return SignedTransaction( txBits = wireTransaction.serialized, - sigs = listOf(NullSignature) + sigs = listOf(NullSignature), + id = wireTransaction.id ) } } diff --git a/finance/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/finance/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index 8ce6d421b8..a943d9f811 100644 --- a/finance/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/finance/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -141,7 +141,7 @@ object TwoPartyTradeProtocol { open fun calculateOurSignature(partialTX: SignedTransaction): DigitalSignature.WithKey { progressTracker.currentStep = SIGNING - return myKeyPair.signWithECDSA(partialTX.txBits) + return myKeyPair.signWithECDSA(partialTX.id) } @Suspendable diff --git a/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt b/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt index 158fad5005..7443574fe8 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NotaryServiceTests.kt @@ -54,7 +54,7 @@ class NotaryServiceTests { val future = runNotaryClient(stx) val signature = future.get() - signature.verifyWithECDSA(stx.txBits) + signature.verifyWithECDSA(stx.id) } @Test fun `should sign a unique transaction without a timestamp`() { @@ -67,7 +67,7 @@ class NotaryServiceTests { val future = runNotaryClient(stx) val signature = future.get() - signature.verifyWithECDSA(stx.txBits) + signature.verifyWithECDSA(stx.id) } @Test fun `should report error for transaction with an invalid timestamp`() { diff --git a/node/src/test/kotlin/com/r3corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/com/r3corda/node/services/persistence/DBTransactionStorageTests.kt index a1732fc7a6..ed46e7ca03 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/persistence/DBTransactionStorageTests.kt @@ -1,13 +1,11 @@ package com.r3corda.node.services.persistence -import com.google.common.primitives.Ints import com.google.common.util.concurrent.SettableFuture import com.r3corda.core.contracts.StateRef import com.r3corda.core.contracts.TransactionType import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.serialization.SerializedBytes import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.transactions.WireTransaction import com.r3corda.core.utilities.DUMMY_NOTARY @@ -144,6 +142,6 @@ class DBTransactionStorageTests { type = TransactionType.General(), timestamp = null ) - return SignedTransaction(wtx.serialized, listOf(DigitalSignature.WithKey(NullPublicKey, ByteArray(1)))) + return SignedTransaction(wtx.serialized, listOf(DigitalSignature.WithKey(NullPublicKey, ByteArray(1))), wtx.id) } } \ No newline at end of file diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/TestDSL.kt b/test-utils/src/main/kotlin/com/r3corda/testing/TestDSL.kt index d73bb6ad1d..dcf30c25ec 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/TestDSL.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/TestDSL.kt @@ -279,10 +279,10 @@ data class TestLedgerDSLInterpreter private constructor ( override fun verifies(): EnforceVerifyOrFail { try { - services.recordTransactions(transactionsUnverified.map { SignedTransaction(it.serialized, listOf(NullSignature)) }) + services.recordTransactions(transactionsUnverified.map { SignedTransaction(it.serialized, listOf(NullSignature), it.id) }) for ((key, value) in transactionWithLocations) { value.transaction.toLedgerTransaction(services).verify() - services.recordTransactions(SignedTransaction(value.transaction.serialized, listOf(NullSignature))) + services.recordTransactions(SignedTransaction(value.transaction.serialized, listOf(NullSignature), value.transaction.id)) } return EnforceVerifyOrFail.Token } catch (exception: TransactionVerificationException) { @@ -326,9 +326,9 @@ fun signAll(transactionsToSign: List, extraKeys: List) } wtx.mustSign.forEach { val key = keyLookup[it] ?: throw IllegalArgumentException("Missing required key for ${it.toStringShort()}") - signatures += key.signWithECDSA(bits) + signatures += key.signWithECDSA(wtx.id) } - SignedTransaction(bits, signatures) + SignedTransaction(bits, signatures, wtx.id) } /**