From 5633b10f5c826e0b2ac47dd85f697f12710898d7 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 24 Oct 2024 10:22:49 +1100 Subject: [PATCH] Manually implement compute_txid and compute_wtxid We would like to move the `Transaction` type over to `primitives` including the `compute_txid` and `compute_wtxid` functions however currently the implementations, as expected, use `Encodable`. Manually implement `Encodable` by hashing all the fields in the correct order. Note we have unit tests already that check the output string of the txid returned so these act as regression tests for this patch. --- bitcoin/src/blockdata/transaction.rs | 71 ++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs index e593a987e..c3760ba71 100644 --- a/bitcoin/src/blockdata/transaction.rs +++ b/bitcoin/src/blockdata/transaction.rs @@ -413,12 +413,8 @@ impl Transaction { /// this will be equal to [`Transaction::compute_wtxid()`]. #[doc(alias = "txid")] pub fn compute_txid(&self) -> Txid { - let mut enc = sha256d::Hash::engine(); - self.version.consensus_encode(&mut enc).expect("engines don't error"); - self.input.consensus_encode(&mut enc).expect("engines don't error"); - self.output.consensus_encode(&mut enc).expect("engines don't error"); - self.lock_time.consensus_encode(&mut enc).expect("engines don't error"); - Txid::from_byte_array(sha256d::Hash::from_engine(enc).to_byte_array()) + let hash = hash_transaction(self, false); + Txid::from_byte_array(hash.to_byte_array()) } /// Computes the segwit version of the transaction id. @@ -435,9 +431,8 @@ impl Transaction { /// this will be equal to [`Transaction::txid()`]. #[doc(alias = "wtxid")] pub fn compute_wtxid(&self) -> Wtxid { - let mut enc = sha256d::Hash::engine(); - self.consensus_encode(&mut enc).expect("engines don't error"); - Wtxid::from_byte_array(sha256d::Hash::from_engine(enc).to_byte_array()) + let hash = hash_transaction(self, self.uses_segwit_serialization()); + Wtxid::from_byte_array(hash.to_byte_array()) } /// Returns the weight of this transaction, as defined by BIP-141. @@ -718,6 +713,64 @@ impl Transaction { } } +// This is equivalent to consensus encoding but hashes the fields manually. +fn hash_transaction(tx: &Transaction, uses_segwit_serialization: bool) -> sha256d::Hash { + use hashes::HashEngine as _; + + let mut enc = sha256d::Hash::engine(); + enc.input(&tx.version.0.to_le_bytes()); // Same as `encode::emit_i32`. + + if uses_segwit_serialization { + // BIP-141 (segwit) transaction serialization also includes marker and flag. + enc.input(&[SEGWIT_MARKER]); + enc.input(&[SEGWIT_FLAG]); + } + + // Encode inputs (excluding witness data) with leading compact size encoded int. + let input_len = tx.input.len(); + enc.input(compact_size::encode(input_len).as_slice()); + for input in &tx.input { + // Encode each input same as we do in `Encodable for TxIn`. + enc.input(input.previous_output.txid.as_byte_array()); + enc.input(&input.previous_output.vout.to_le_bytes()); + + let script_sig_bytes = input.script_sig.as_bytes(); + enc.input(compact_size::encode(script_sig_bytes.len()).as_slice()); + enc.input(script_sig_bytes); + + enc.input(&input.sequence.0.to_le_bytes()) + } + + // Encode outputs with leading compact size encoded int. + let output_len = tx.output.len(); + enc.input(compact_size::encode(output_len).as_slice()); + for output in &tx.output { + // Encode each output same as we do in `Encodable for TxOut`. + enc.input(&output.value.to_sat().to_le_bytes()); + + let script_pubkey_bytes = output.script_pubkey.as_bytes(); + enc.input(compact_size::encode(script_pubkey_bytes.len()).as_slice()); + enc.input(script_pubkey_bytes); + } + + if uses_segwit_serialization { + // BIP-141 (segwit) transaction serialization also includes the witness data. + for input in &tx.input { + // Same as `Encodable for Witness`. + enc.input(compact_size::encode(input.witness.len()).as_slice()); + for element in input.witness.iter() { + enc.input(compact_size::encode(element.len()).as_slice()); + enc.input(element); + } + } + } + + // Same as `Encodable for absolute::LockTime`. + enc.input(&tx.lock_time.to_consensus_u32().to_le_bytes()); + + sha256d::Hash::from_engine(enc) +} + /// Error attempting to do an out of bounds access on the transaction inputs vector. #[derive(Debug, Clone, PartialEq, Eq)] pub struct InputsIndexError(pub IndexOutOfBoundsError);