fuzz: Enforce additional version invariants in PSBT fuzzer
What changed, and why it matters
This commit only changes test/fuzzing code and adds equality operators to internal data structures so the fuzzer can check round-trip behavior of PSBT (Partially Signed Bitcoin Transaction) data. It does not change production transaction handling, consensus rules, or network code. There is no indication it fixes an exploitable vulnerability.
No production action required. Treat as normal test/fuzzing improvement. If auditing, verify that the new equality operators do not accidentally affect production behavior (they are defaulted and only used for comparisons).
Security signals we found
Fuzzer invariant hardening (not a runtime security fix)
Adds equality operators to PSBTInput, PSBTOutput, CScriptWitness for test assertions
Asserts PSBT version is 0 or 2 in fuzz target
Asserts input/output round-trip through SignatureData
Evidence from the diff
The patch extends the existing PSBT fuzz target (src/test/fuzz/psbt.cpp) to assert additional invariants: PSBT version must be 0 or 2, inputs/outputs round-trip through SignatureData correctly, and unnecessary transactions are removed after a merge. It adds defaulted operator== to PSBTInput, PSBTOutput, and CScriptWitness so the fuzzer can compare objects. These are test-only strengthening changes; no production parsing or validation logic is modified.
Changed components
src/test/fuzz/psbt.cppsrc/psbt.hsrc/script/script.hInspect captured patch +66 / −8
diff --git a/src/psbt.h b/src/psbt.h
index bca99953..de57b569 100644
--- a/src/psbt.h
+++ b/src/psbt.h
@@ -353,6 +353,8 @@ public:
Unserialize(s);
}
+ bool operator==(const PSBTInput&) const = default;
+
template <typename Stream>
inline void Serialize(Stream& s) const {
// Write the utxo
@@ -976,6 +978,8 @@ public:
Unserialize(s);
}
+ bool operator==(const PSBTOutput&) const = default;
+
template <typename Stream>
inline void Serialize(Stream& s) const {
// Write the redeem script
diff --git a/src/script/script.h b/src/script/script.h
index b06be9c9..674034c6 100644
--- a/src/script/script.h
+++ b/src/script/script.h
@@ -587,6 +587,8 @@ struct CScriptWitness
void SetNull() { stack.clear(); stack.shrink_to_fit(); }
std::string ToString() const;
+
+ bool operator==(const CScriptWitness&) const = default;
};
/** A reference to a CScript: the Hash160 of its serialization */
diff --git a/src/test/fuzz/psbt.cpp b/src/test/fuzz/psbt.cpp
index 7d0f775b..d3cb4954 100644
--- a/src/test/fuzz/psbt.cpp
+++ b/src/test/fuzz/psbt.cpp
@@ -33,6 +33,10 @@ FUZZ_TARGET(psbt)
PartiallySignedTransaction psbt_mut = *psbt_res;
const PartiallySignedTransaction psbt = psbt_mut;
+ // We are on purpose not forward compatible, and version 1 is disabled.
+ const auto psbt_version{psbt.GetVersion()};
+ Assert(psbt_version == 0 || psbt_version == 2);
+
// A PSBT must roundtrip.
std::vector<uint8_t> psbt_ser;
VectorWriter{psbt_ser, 0, psbt};
@@ -51,25 +55,71 @@ FUZZ_TARGET(psbt)
}
(void)psbt.IsNull();
-
(void)psbt.GetUnsignedTx();
for (const PSBTInput& input : psbt.inputs) {
(void)PSBTInputSigned(input);
(void)input.IsNull();
+ PSBTInput input_mod = input;
+ CTxOut tx_out;
+ if (input.GetUTXO(tx_out)) {
+ (void)tx_out.IsNull();
+ (void)tx_out.ToString();
+ }
+ // A PSBT input must roundtrip to signature data.
+ PSBTInput input_fill{psbt_version, input_mod.prev_txid, input_mod.prev_out, input_mod.sequence};
+ SignatureData sig_data;
+ input_mod.FillSignatureData(sig_data);
+ input_fill.FromSignatureData(sig_data);
+
+ // Only final_script_sig and final_script_witness are filled when sigdata is complete
+ if (sig_data.complete) {
+ Assert(input_mod.final_script_sig == input_fill.final_script_sig);
+ Assert(input_mod.final_script_witness == input_fill.final_script_witness);
+ } else {
+ // UTXOs don't go into SignatureData
+ input_mod.non_witness_utxo.reset();
+ input_mod.witness_utxo.SetNull();
+ // Sighash type doesn't go into SignatureData
+ input_mod.sighash_type.reset();
+ // Timelocks don't go into SignatureData
+ input_mod.time_locktime.reset();
+ input_mod.height_locktime.reset();
+ // Proprietary fields are not included in SignatureData
+ input_mod.m_proprietary.clear();
+ // Unknown fields are not included in SignatureData
+ input_mod.unknown.clear();
+
+ Assert(input_mod == input_fill);
+ }
}
(void)CountPSBTUnsignedInputs(psbt);
for (const PSBTOutput& output : psbt.outputs) {
(void)output.IsNull();
- }
+ PSBTOutput output_mod = output;
+ // A PSBT output must roundtrip to signature data.
+ PSBTOutput output_fill{psbt_version, output_mod.amount, output_mod.script};
+ SignatureData sig_data;
+ output_mod.FillSignatureData(sig_data);
+ output_fill.FromSignatureData(sig_data);
- for (const PSBTInput& input : psbt.inputs) {
- CTxOut tx_out;
- if (input.GetUTXO(tx_out)) {
- (void)tx_out.IsNull();
- (void)tx_out.ToString();
+ // FillSignatureData will not fill tap tree or internal key if the tree is empty or
+ // the key is not fully valid. These need to be cleared before checking for equivalence
+ if (output_mod.m_tap_tree.empty() || !output_mod.m_tap_internal_key.IsFullyValid()) {
+ output_mod.m_tap_tree.clear();
+ std::fill(output_mod.m_tap_internal_key.begin(), output_mod.m_tap_internal_key.end(), 0);
}
+ // Sort m_tap_tree to ensure the vectors match
+ std::sort(output_mod.m_tap_tree.begin(), output_mod.m_tap_tree.end());
+ std::sort(output_fill.m_tap_tree.begin(), output_fill.m_tap_tree.end());
+ // Proprietary fields are not included in SignatureData
+ output_mod.m_proprietary.clear();
+ // Unknown fields are not included in SignatureData
+ output_mod.unknown.clear();
+
+ Assert(output_mod.m_tap_internal_key == output_fill.m_tap_internal_key);
+ Assert(output_mod == output_fill);
}
psbt_mut = psbt;
@@ -98,7 +148,9 @@ FUZZ_TARGET(psbt)
(void)psbt_mut.AddInput(psbt_in);
}
for (const auto& psbt_out : psbt_merge.outputs) {
- Assert(psbt_mut.AddOutput(psbt_out));
+ (void)psbt_mut.AddOutput(psbt_out);
}
psbt_mut.unknown.insert(psbt_merge.unknown.begin(), psbt_merge.unknown.end());
+
+ RemoveUnnecessaryTransactions(psbt_mut);
}
Why this scored 17/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.