What changed, and why it matters
This is a routine API refactor in a Rust Bitcoin library. It introduces a new public type called Ntxid to represent a 'normalized transaction ID' instead of returning a generic hash. The goal is to let the library stabilize one of its core modules without forcing users to depend on another internal module. There is no security bug being fixed here.
No security action needed. Reviewers may want to verify downstream consumers handle the changed return type, but this is a normal API migration.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit adds a new Ntxid newtype wrapping sha256d::Hash in primitives/src/transaction.rs, re-exports it, and changes the deprecated TransactionExt::ntxid() and Transaction::compute_ntxid() return types from sha256d::Hash to Ntxid. It also expands hex/debug/serde trait implementations to include Ntxid. The computation logic is unchanged: it still clones the transaction, clears each input’s script_sig, computes the txid, and wraps the resulting bytes. The commit is purely a type-system/API change to decouple primitives from the hashes crate’s public API.
Changed components
primitives/src/transaction.rsprimitives/src/lib.rsbitcoin/src/blockdata/transaction.rsInspect captured patch +30 / −13
diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index eb5a0dbd..8ac8ac2f 100644
--- a/bitcoin/src/blockdata/transaction.rs
+++ b/bitcoin/src/blockdata/transaction.rs
@@ -14,7 +14,6 @@ use core::fmt;
#[cfg(feature = "arbitrary")]
use arbitrary::{Arbitrary, Unstructured};
-use hashes::sha256d;
use internals::{compact_size, const_casts, write_err, ToU64};
use io::{BufRead, Write};
use primitives::Sequence;
@@ -31,7 +30,7 @@ use crate::{internal_macros, Amount, FeeRate, SignedAmount};
#[rustfmt::skip] // Keep public re-exports separate.
#[doc(inline)]
-pub use primitives::transaction::{OutPoint, ParseOutPointError, Transaction, Txid, Wtxid, Version, TxIn, TxOut};
+pub use primitives::transaction::{OutPoint, ParseOutPointError, Transaction, Ntxid, Txid, Wtxid, Version, TxIn, TxOut};
internal_macros::impl_hashencode!(Txid);
internal_macros::impl_hashencode!(Wtxid);
@@ -215,7 +214,7 @@ fn size_from_script_pubkey(script_pubkey: &Script) -> usize {
pub trait TransactionExt: sealed::Sealed {
/// Computes a "normalized TXID" which does not include any signatures.
#[deprecated(since = "0.31.0", note = "use `compute_ntxid()` instead")]
- fn ntxid(&self) -> sha256d::Hash;
+ fn ntxid(&self) -> Ntxid;
/// Computes the [`Txid`].
#[deprecated(since = "0.31.0", note = "use `compute_txid()` instead")]
@@ -334,7 +333,7 @@ pub trait TransactionExt: sealed::Sealed {
}
impl TransactionExt for Transaction {
- fn ntxid(&self) -> sha256d::Hash { self.compute_ntxid() }
+ fn ntxid(&self) -> Ntxid { self.compute_ntxid() }
fn txid(&self) -> Txid { self.compute_txid() }
diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs
index 4aa1f323..c0a60c78 100644
--- a/primitives/src/lib.rs
+++ b/primitives/src/lib.rs
@@ -83,7 +83,7 @@ pub use self::{
block::{BlockHash, Header as BlockHeader, Version as BlockVersion, WitnessCommitment},
merkle_tree::{TxMerkleNode, WitnessMerkleNode},
pow::CompactTarget,
- transaction::{OutPoint, Txid, Version as TransactionVersion, Wtxid},
+ transaction::{Ntxid, OutPoint, Txid, Version as TransactionVersion, Wtxid},
};
#[rustfmt::skip]
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index 3ca18d15..0a916778 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -121,11 +121,16 @@ impl Transaction {
/// Computes a "normalized TXID" which does not include any signatures.
///
- /// This gives a way to identify a transaction that is "the same" as
- /// another in the sense of having same inputs and outputs.
+ /// This function is needed only for legacy (pre-Segwit or P2SH-wrapped segwit version 0)
+ /// applications. This method clears the `script_sig` field of each input, which in Segwit
+ /// transactions is already empty, so for Segwit transactions the ntxid will be equal to the
+ /// txid, and you should simply use the latter.
+ ///
+ /// This gives a way to identify a transaction that is "the same" as another in the sense of
+ /// having the same inputs and outputs.
#[doc(alias = "ntxid")]
- pub fn compute_ntxid(&self) -> sha256d::Hash {
- let cloned_tx = Transaction {
+ pub fn compute_ntxid(&self) -> Ntxid {
+ let normalized = Transaction {
version: self.version,
lock_time: self.lock_time,
inputs: self
@@ -139,7 +144,7 @@ impl Transaction {
.collect(),
outputs: self.outputs.clone(),
};
- sha256d::Hash::from_byte_array(cloned_tx.compute_txid().to_byte_array())
+ Ntxid::from_byte_array(normalized.compute_txid().to_byte_array())
}
/// Computes the [`Txid`].
@@ -486,14 +491,27 @@ hashes::hash_newtype! {
/// A bitcoin witness transaction ID.
pub struct Wtxid(sha256d::Hash);
+
+ /// A "normalized TXID".
+ ///
+ /// Computed on a transaction that has had the signatures removed.
+ ///
+ /// This type is needed only for legacy (pre-Segwit or P2SH-wrapped segwit version 0)
+ /// applications. This method clears the `script_sig` field of each input, which in Segwit
+ /// transactions is already empty, so for Segwit transactions the ntxid will be equal to the
+ /// txid, and you should simply use the latter.
+ ///
+ /// This gives a way to identify a transaction that is "the same" as another in the sense of
+ /// having the same inputs and outputs.
+ pub struct Ntxid(sha256d::Hash);
}
#[cfg(feature = "hex")]
-hashes::impl_hex_for_newtype!(Txid, Wtxid);
+hashes::impl_hex_for_newtype!(Txid, Wtxid, Ntxid);
#[cfg(not(feature = "hex"))]
-hashes::impl_debug_only_for_newtype!(Txid, Wtxid);
+hashes::impl_debug_only_for_newtype!(Txid, Wtxid, Ntxid);
#[cfg(feature = "serde")]
-hashes::impl_serde_for_newtype!(Txid, Wtxid);
+hashes::impl_serde_for_newtype!(Txid, Wtxid, Ntxid);
impl Txid {
/// The `Txid` used in a coinbase prevout.
Why this scored 18/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.