Adjust Transaction arbitrary to satisfy decode checks
What changed, and why it matters
This commit fixes a fuzz-testing helper so that randomly generated fake Bitcoin transactions follow the same rules that the real transaction decoder enforces. It does not change normal transaction handling, network behavior, or wallet logic; it only affects test code used to fuzz the library.
No security action required. Treat as a normal test-quality improvement. If auditing, verify that the production TransactionDecoder invariants remain unchanged and that this patch only touches the Arbitrary impl.
Security signals we found
Fuzz-only code path
Alignment of arbitrary generator with existing decoder invariants
No change to consensus-critical decode/validation logic
No network, serialization, or cryptographic code modified
Evidence from the diff
The Arbitrary implementation for Transaction in primitives/src/transaction.rs is updated to enforce invariants that TransactionDecoder already requires: no duplicate inputs, at most one coinbase-style prevout with a script_sig length between 2 and 100 bytes, at least one output, and total output value not exceeding MAX_MONEY. This prevents fuzz-generated transactions from failing round-trip decode/encode checks. The change is confined to a test-only trait impl behind the alloc feature.
Changed components
primitives/src/transaction.rsArbitrary impl for Transaction (fuzz/test-only)Inspect captured patch +31 / −6
diff --git a/primitives/src/transaction.rs b/primitives/src/transaction.rs
index c9cdfcd5..da9a66a3 100644
--- a/primitives/src/transaction.rs
+++ b/primitives/src/transaction.rs
@@ -1522,12 +1522,37 @@ pub mod error {
#[cfg(feature = "alloc")]
impl<'a> Arbitrary<'a> for Transaction {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
- Ok(Self {
- version: Version::arbitrary(u)?,
- lock_time: absolute::LockTime::arbitrary(u)?,
- inputs: Vec::<TxIn>::arbitrary(u)?,
- outputs: Vec::<TxOut>::arbitrary(u)?,
- })
+ let version = Version::arbitrary(u)?;
+ let lock_time = absolute::LockTime::arbitrary(u)?;
+ let mut inputs = Vec::<TxIn>::arbitrary(u)?;
+ let mut outputs = Vec::<TxOut>::arbitrary(u)?;
+
+ let mut seen = alloc::collections::BTreeSet::new();
+ inputs.retain(|input| seen.insert(input.previous_output));
+
+ if inputs.len() > 1 {
+ inputs.retain(|input| input.previous_output != OutPoint::COINBASE_PREVOUT);
+ }
+
+ if inputs.len() == 1 && inputs[0].previous_output == OutPoint::COINBASE_PREVOUT {
+ let len = inputs[0].script_sig.len();
+ if len < 2 || len > 100 {
+ inputs[0].script_sig = ScriptSigBuf::from_bytes(Vec::from([0u8; 2]));
+ }
+ }
+
+ if outputs.is_empty() {
+ outputs.push(TxOut::arbitrary(u)?);
+ }
+
+ let mut remaining = Amount::MAX_MONEY.to_sat();
+ for output in &mut outputs {
+ let capped = output.amount.to_sat().min(remaining);
+ output.amount = Amount::from_sat(capped).expect("capped <= MAX_MONEY");
+ remaining -= capped;
+ }
+
+ Ok(Self { version, lock_time, inputs, outputs })
}
}
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.