Replace old encoding logic with new encoders in sighash
What changed, and why it matters
This is a code cleanup commit in the rust-bitcoin library. It replaces old transaction-encoding helper functions with newer equivalent ones inside the signature-hash (sighash) code. There is no direct evidence in the commit message or diff that this fixes a security vulnerability; it reads as a refactoring to use a newer internal API. However, because the change touches the exact code that computes Bitcoin transaction signatures, any accidental change to the bytes produced could break compatibility with the Bitcoin protocol or wallets, so it is in a security-sensitive area.
Treat as a refactoring in a high-risk module. Reviewers should verify that the new encoders produce byte-for-byte identical output for all sighash types, that the public API signature change is intentional and acceptable, and that the change is covered by existing or new sighash unit tests and cross-implementation vectors before release.
Security signals we found
Touches consensus-critical serialization code (sighash)
Changes public API signatures (writer parameter from &mut W to mut W)
Replaces hash-engine encoding helpers with new crate equivalents
No explicit security claim, CVE, bug fix, or test update in commit metadata
Evidence from the diff
The commit refactors bitcoin/src/crypto/sighash.rs to remove calls to the legacy consensus::Encodable::consensus_encode and replace them with new consensus_encoding crate helpers (io::encode_to_writer, hashes::encode_to_engine, encoding::Encoder2, direct write_all calls for fixed-width primitives). It also changes several public method signatures from &mut W where W: Write + ?Sized to mut writer W where W: Write, removing object-safety/dyn compatibility. The logic paths (taproot, segwit v0, legacy) and the serialized byte layout are intended to remain identical. No test changes are shown in the supplied diff, and the commit message does not describe a bug or security fix.
Changed components
bitcoin/src/crypto/sighash.rsSighashCache::taproot_encode_signing_data_toSighashCache::segwit_v0_encode_signing_data_toSighashCache::legacy_encode_signing_data_toScriptPath::leaf_hashinternal sighash cache computationInspect captured patch +83 / −72
diff --git a/bitcoin/src/crypto/sighash.rs b/bitcoin/src/crypto/sighash.rs
index 3b5db091..4f990605 100644
--- a/bitcoin/src/crypto/sighash.rs
+++ b/bitcoin/src/crypto/sighash.rs
@@ -339,11 +339,10 @@ impl<'s> ScriptPath<'s> {
pub fn leaf_hash(&self) -> TapLeafHash {
let mut enc = sha256t::Hash::<TapLeafTag>::engine();
- self.leaf_version
- .to_consensus()
- .consensus_encode(&mut enc)
+ enc
+ .write_all(&[self.leaf_version.to_consensus()])
.expect("writing to hash engine should never fail");
- self.script.consensus_encode(&mut enc).expect("writing to hash engine should never fail");
+ enc = hashes::encode_to_engine(self.script, enc);
let inner = sha256t::Hash::<TapLeafTag>::from_engine(enc);
TapLeafHash::from_byte_array(inner.to_byte_array())
@@ -615,9 +614,9 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
/// In order to sign, the data written by this function must be hashed using a tagged hash. For
/// example usage see [`Self::taproot_signature_hash`] and
/// [`Self::taproot_key_spend_signature_hash`].
- pub fn taproot_encode_signing_data_to<W: Write + ?Sized, T: Borrow<TxOut>>(
+ pub fn taproot_encode_signing_data_to<W: Write, T: Borrow<TxOut>>(
&mut self,
- writer: &mut W,
+ mut writer: W,
input_index: usize,
prevouts: &Prevouts<T>,
annex: Option<Annex>,
@@ -629,18 +628,18 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
let (sighash, anyone_can_pay) = sighash_type.split_anyonecanpay_flag();
// epoch
- 0u8.consensus_encode(writer)?;
+ writer.write_all(&[0u8])?;
// * Control:
// hash_type (1).
- (sighash_type as u8).consensus_encode(writer)?;
+ writer.write_all(&[sighash_type as u8])?;
// * Transaction Data:
// nVersion (4): the nVersion of the transaction.
- self.tx.borrow().version.consensus_encode(writer)?;
+ io::encode_to_writer(&self.tx.borrow().version, &mut writer)?;
// nLockTime (4): the nLockTime of the transaction.
- self.tx.borrow().lock_time.consensus_encode(writer)?;
+ io::encode_to_writer(&self.tx.borrow().lock_time, &mut writer)?;
// If the hash_type & 0x80 does not equal SIGHASH_ANYONECANPAY:
// sha_prevouts (32): the SHA256 of the serialization of all input outpoints.
@@ -648,20 +647,22 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
// sha_scriptpubkeys (32): the SHA256 of the serialization of all spent output scriptPubKeys.
// sha_sequences (32): the SHA256 of the serialization of all input nSequence.
if !anyone_can_pay {
- self.common_cache().prevouts.consensus_encode(writer)?;
- self.taproot_cache(prevouts.get_all().map_err(SigningDataError::sighash)?)
- .amounts
- .consensus_encode(writer)?;
- self.taproot_cache(prevouts.get_all().map_err(SigningDataError::sighash)?)
- .script_pubkeys
- .consensus_encode(writer)?;
- self.common_cache().sequences.consensus_encode(writer)?;
+ writer.write_all(&self.common_cache().prevouts.to_byte_array())?;
+ writer.write_all(
+ &self.taproot_cache(prevouts.get_all().map_err(SigningDataError::sighash)?).amounts.to_byte_array()
+ )?;
+ writer.write_all(
+ &self.taproot_cache(prevouts.get_all().map_err(SigningDataError::sighash)?)
+ .script_pubkeys
+ .to_byte_array()
+ )?;
+ writer.write_all(&self.common_cache().sequences.to_byte_array())?;
}
// If hash_type & 3 does not equal SIGHASH_NONE or SIGHASH_SINGLE:
// sha_outputs (32): the SHA256 of the serialization of all outputs in CTxOut format.
if sighash != TapSighashType::None && sighash != TapSighashType::Single {
- self.common_cache().outputs.consensus_encode(writer)?;
+ writer.write_all(&self.common_cache().outputs.to_byte_array())?;
}
// * Data about this input:
@@ -674,7 +675,7 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
if leaf_hash_code_separator.is_some() {
spend_type |= 2u8;
}
- spend_type.consensus_encode(writer)?;
+ writer.write_all(&[spend_type])?;
// If hash_type & 0x80 equals SIGHASH_ANYONECANPAY:
// outpoint (36): the COutPoint of this input (32-byte hash + 4-byte little-endian).
@@ -684,12 +685,12 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
if anyone_can_pay {
let txin = &self.tx.borrow().tx_in(input_index).map_err(SigningDataError::sighash)?;
let previous_output = prevouts.get(input_index).map_err(SigningDataError::sighash)?;
- txin.previous_output.consensus_encode(writer)?;
- previous_output.amount.consensus_encode(writer)?;
- previous_output.script_pubkey.consensus_encode(writer)?;
- txin.sequence.consensus_encode(writer)?;
+ io::encode_to_writer(&txin.previous_output, &mut writer)?;
+ io::encode_to_writer(&previous_output.amount, &mut writer)?;
+ io::encode_to_writer(&(*previous_output.script_pubkey), &mut writer)?;
+ io::encode_to_writer(&txin.sequence, &mut writer)?;
} else {
- (input_index as u32).consensus_encode(writer)?;
+ writer.write_all(&(input_index as u32).to_le_bytes())?;
}
// If an annex is present (the lowest bit of spend_type is set):
@@ -697,9 +698,15 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
// includes the mandatory 0x50 prefix.
if let Some(annex) = annex {
let mut enc = sha256::Hash::engine();
- annex.consensus_encode(&mut enc)?;
+ // Use an ad-hoc encoder to write to the hash engine, since annex has no
+ // true consensus encoding format.
+ let mut encoder = encoding::Encoder2::new(
+ encoding::CompactSizeEncoder::new(annex.0.len()),
+ encoding::BytesEncoder::without_length_prefix(annex.0),
+ );
+ io::flush_to_writer(&mut encoder, &mut enc)?;
let hash = sha256::Hash::from_engine(enc);
- hash.consensus_encode(writer)?;
+ writer.write_all(&hash.to_byte_array())?;
}
// * Data about this output:
@@ -707,7 +714,7 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
// sha_single_output (32): the SHA256 of the corresponding output in CTxOut format.
if sighash == TapSighashType::Single {
let mut enc = sha256::Hash::engine();
- self.tx
+ let txout = self.tx
.borrow()
.outputs
.get(input_index)
@@ -715,10 +722,10 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
input_index,
outputs_length: self.tx.borrow().outputs.len(),
}))
- .map_err(SigningDataError::Sighash)?
- .consensus_encode(&mut enc)?;
+ .map_err(SigningDataError::Sighash)?;
+ enc = hashes::encode_to_engine(txout, enc);
let hash = sha256::Hash::from_engine(enc);
- hash.consensus_encode(writer)?;
+ writer.write_all(&hash.to_byte_array())?;
}
// if (scriptpath):
@@ -726,9 +733,9 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
// ss += bytes([0])
// ss += struct.pack("<i", codeseparator_pos)
if let Some((hash, code_separator_pos)) = leaf_hash_code_separator {
- hash.as_byte_array().consensus_encode(writer)?;
- KEY_VERSION_0.consensus_encode(writer)?;
- code_separator_pos.consensus_encode(writer)?;
+ writer.write_all(hash.as_byte_array())?;
+ writer.write_all(&[KEY_VERSION_0])?;
+ writer.write_all(&code_separator_pos.to_le_bytes())?;
}
Ok(())
@@ -813,9 +820,9 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
/// In order to sign, the data written by this function must be hashed using a double SHA256
/// hash. This can be achieved either by using the [`hashes::sha256d::Hash`] type or one of the
/// custom sighash types in this module ([`SegwitV0Sighash`] and [`LegacySighash`]).
- pub fn segwit_v0_encode_signing_data_to<W: Write + ?Sized>(
+ pub fn segwit_v0_encode_signing_data_to<W: Write>(
&mut self,
- writer: &mut W,
+ mut writer: W,
input_index: usize,
script_code: &WitnessScript,
amount: Amount,
@@ -825,46 +832,46 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
let (sighash, anyone_can_pay) = sighash_type.split_anyonecanpay_flag();
- self.tx.borrow().version.consensus_encode(writer)?;
+ io::encode_to_writer(&self.tx.borrow().version, &mut writer)?;
if !anyone_can_pay {
- self.segwit_cache().prevouts.consensus_encode(writer)?;
+ writer.write_all(&self.segwit_cache().prevouts.to_byte_array())?;
} else {
- zero_hash.consensus_encode(writer)?;
+ writer.write_all(&zero_hash)?;
}
if !anyone_can_pay
&& sighash != EcdsaSighashType::Single
&& sighash != EcdsaSighashType::None
{
- self.segwit_cache().sequences.consensus_encode(writer)?;
+ writer.write_all(&self.segwit_cache().sequences.to_byte_array())?;
} else {
- zero_hash.consensus_encode(writer)?;
+ writer.write_all(&zero_hash)?;
}
{
let txin = &self.tx.borrow().tx_in(input_index).map_err(SigningDataError::sighash)?;
- txin.previous_output.consensus_encode(writer)?;
- script_code.consensus_encode(writer)?;
- amount.consensus_encode(writer)?;
- txin.sequence.consensus_encode(writer)?;
+ io::encode_to_writer(&txin.previous_output, &mut writer)?;
+ io::encode_to_writer(script_code, &mut writer)?;
+ io::encode_to_writer(&amount, &mut writer)?;
+ io::encode_to_writer(&txin.sequence, &mut writer)?;
}
if sighash != EcdsaSighashType::Single && sighash != EcdsaSighashType::None {
- self.segwit_cache().outputs.consensus_encode(writer)?;
+ writer.write_all(&self.segwit_cache().outputs.to_byte_array())?;
} else if sighash == EcdsaSighashType::Single
&& input_index < self.tx.borrow().outputs.len()
{
let mut single_enc = LegacySighash::engine();
- self.tx.borrow().outputs[input_index].consensus_encode(&mut single_enc)?;
+ single_enc = hashes::encode_to_engine(&self.tx.borrow().outputs[input_index], single_enc);
let hash = LegacySighash::from_engine(single_enc);
writer.write_all(hash.as_byte_array())?;
} else {
writer.write_all(&zero_hash)?;
}
- self.tx.borrow().lock_time.consensus_encode(writer)?;
- sighash_type.to_u32().consensus_encode(writer)?;
+ io::encode_to_writer(&self.tx.borrow().lock_time, &mut writer)?;
+ writer.write_all(&sighash_type.to_u32().to_le_bytes())?;
Ok(())
}
@@ -938,9 +945,9 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
///
/// This function can't handle the SIGHASH_SINGLE bug internally, so it returns [`EncodeSigningDataResult`]
/// that must be handled by the caller (see [`EncodeSigningDataResult::is_sighash_single_bug`]).
- pub fn legacy_encode_signing_data_to<W: Write + ?Sized, U: Into<u32>, T: ScriptHashableTag>(
+ pub fn legacy_encode_signing_data_to<W: Write, U: Into<u32>, T: ScriptHashableTag>(
&self,
- writer: &mut W,
+ mut writer: W,
input_index: usize,
script_pubkey: &crate::script::Script<T>,
sighash_type: U,
@@ -963,9 +970,9 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
return EncodeSigningDataResult::SighashSingleBug;
}
- fn encode_signing_data_to_inner<W: Write + ?Sized, T: ScriptHashableTag>(
+ fn encode_signing_data_to_inner<W: Write, T: ScriptHashableTag>(
self_: &Transaction,
- writer: &mut W,
+ mut writer: W,
input_index: usize,
script_pubkey: &crate::script::Script<T>,
sighash_type: u32,
@@ -975,36 +982,40 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
let (sighash, anyone_can_pay) =
EcdsaSighashType::from_consensus(sighash_type).split_anyonecanpay_flag();
- self_.version.consensus_encode(writer)?;
+ io::encode_to_writer(&self_.version, &mut writer)?;
// Add all inputs necessary..
if anyone_can_pay {
writer.emit_compact_size(1u8)?;
- self_.inputs[input_index].previous_output.consensus_encode(writer)?;
- script_pubkey.consensus_encode(writer)?;
- self_.inputs[input_index].sequence.consensus_encode(writer)?;
+ io::encode_to_writer(&self_.inputs[input_index].previous_output, &mut writer)?;
+ io::encode_to_writer(script_pubkey, &mut writer)?;
+ io::encode_to_writer(&self_.inputs[input_index].sequence, &mut writer)?;
} else {
writer.emit_compact_size(self_.inputs.len())?;
for (n, input) in self_.inputs.iter().enumerate() {
- input.previous_output.consensus_encode(writer)?;
+ io::encode_to_writer(&input.previous_output, &mut writer)?;
if n == input_index {
- script_pubkey.consensus_encode(writer)?;
+ io::encode_to_writer(script_pubkey, &mut writer)?;
} else {
- ScriptPubKey::new().consensus_encode(writer)?;
+ io::encode_to_writer(ScriptPubKey::new(), &mut writer)?;
}
if n != input_index
&& (sighash == EcdsaSighashType::Single
|| sighash == EcdsaSighashType::None)
{
- Sequence::ZERO.consensus_encode(writer)?;
+ io::encode_to_writer(&Sequence::ZERO, &mut writer)?;
} else {
- input.sequence.consensus_encode(writer)?;
+ io::encode_to_writer(&input.sequence, &mut writer)?;
}
}
}
// ..then all outputs
match sighash {
EcdsaSighashType::All => {
- self_.outputs.consensus_encode(writer)?;
+ let mut encoder = encoding::Encoder2::new(
+ encoding::CompactSizeEncoder::new(self_.outputs.len()),
+ encoding::SliceEncoder::without_length_prefix(&self_.outputs),
+ );
+ io::flush_to_writer(&mut encoder, &mut writer)?;
}
EcdsaSighashType::Single => {
// sign all outputs up to and including this one, but erase
@@ -1016,22 +1027,22 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
writer
.write_all(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00])?;
}
- self_.outputs[count].consensus_encode(writer)?;
+ io::encode_to_writer(&self_.outputs[count], &mut writer)?;
}
EcdsaSighashType::None => {
writer.emit_compact_size(0u8)?;
}
_ => unreachable!(),
};
- self_.lock_time.consensus_encode(writer)?;
- sighash_type.to_le_bytes().consensus_encode(writer)?;
+ io::encode_to_writer(&self_.lock_time, &mut writer)?;
+ writer.write_all(&sighash_type.to_le_bytes())?;
Ok(())
}
EncodeSigningDataResult::WriteResult(
encode_signing_data_to_inner(
self.tx.borrow(),
- writer,
+ &mut writer,
input_index,
script_pubkey,
sighash_type,
@@ -1089,8 +1100,8 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
let mut enc_prevouts = sha256::Hash::engine();
let mut enc_sequences = sha256::Hash::engine();
for txin in tx.inputs.iter() {
- txin.previous_output.consensus_encode(&mut enc_prevouts).unwrap();
- txin.sequence.consensus_encode(&mut enc_sequences).unwrap();
+ enc_prevouts = hashes::encode_to_engine(&txin.previous_output, enc_prevouts);
+ enc_sequences = hashes::encode_to_engine(&txin.sequence, enc_sequences);
}
CommonCache {
prevouts: sha256::Hash::from_engine(enc_prevouts),
@@ -1098,7 +1109,7 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
outputs: {
let mut enc = sha256::Hash::engine();
for txout in tx.outputs.iter() {
- txout.consensus_encode(&mut enc).unwrap();
+ io::encode_to_writer(txout, &mut enc).unwrap();
}
sha256::Hash::from_engine(enc)
},
@@ -1124,8 +1135,8 @@ impl<R: Borrow<Transaction>> SighashCache<R> {
let mut enc_amounts = sha256::Hash::engine();
let mut enc_script_pubkeys = sha256::Hash::engine();
for prevout in prevouts {
- prevout.borrow().amount.consensus_encode(&mut enc_amounts).unwrap();
- prevout.borrow().script_pubkey.consensus_encode(&mut enc_script_pubkeys).unwrap();
+ enc_amounts = hashes::encode_to_engine(&prevout.borrow().amount, enc_amounts);
+ enc_script_pubkeys = hashes::encode_to_engine(&(*prevout.borrow().script_pubkey), enc_script_pubkeys);
}
TaprootCache {
amounts: sha256::Hash::from_engine(enc_amounts),
Why this scored 16/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.