#35662 script: prevent partial reinitialization of sighash data

ReviewabilityReviewsAgreementSize
#35662 l0rinc script: prevent partial reinitialization of sighash dataNeeds rebase1Strong35 + 55 tests
  • Prevent transactions from accidentally reusing cached signature verification data across transactions
  • Eliminate unnecessary copying in kernel verification and skip unused block verification memory
  • Ready for review with no known blockers or invalidating changes
  • Strong support with all reviewer concerns addressed
  • Verified by differential testing across thousands of sighash key paths and kernel benchmarks (fametrano)
  • Approval after objection to enforce initialization at construction was resolved (sedited)
  • 35 lines added or modified outside tests
  • 55 lines added or modified in tests
  • 88 lines removed in total
  • 17 files, 5 commits
Kernel (libbitcoinkernel)#12 of 18
P3 · speedup
  • P3 because it speeds up the experimental kernel verification API by orders of magnitude
  • The speedup comes from eliminating an unintended transaction data copy on every call
  • Its impact is limited to consumers of the experimental kernel C interface
Validation#38 of 53
P3 · cleanup
  • P3 because it removes an error-prone pattern that could corrupt signature hashes if caches were reused
  • It also avoids allocating unused verification data during assumevalid block connection
  • Ranking pass: Moved down: hardening of an error-prone pattern, no active bug, sits with the other defensive validation refactors
Reviewability: ReadyStalePaused
Agreement: StrongPositivePositive w/ caveatsNeutralMildDisputedBlockedCrickets
Reviews: current code-review ACKs, then (+stale ACKs) and -NACKs; greener = more ACKs.
Size: lines added or modified outside tests, then in tests; greener = smaller.

Goal

Removes default construction and the public `Init()` method from `PrecomputedTransactionData`, requiring transaction context and optional spent outputs at construction time. Validation and test call sites that defer precomputation now use `std::optional<PrecomputedTransactionData>`, emplacing the object only after a script-cache miss. The PR also skips txdata allocation during block connection when assumevalid bypasses script checks, and eliminates an unintended copy of caller-supplied txdata in the kernel C API.

Problem: Default-constructing `PrecomputedTransactionData` and initializing it later through `Init()` allowed instances to exist uninitialized or be reused across transactions. The old `Init()` safety guard checked only `m_spent_outputs_ready`, which stayed false after forced initialization without spent outputs even though BIP143 sighash hashes were cached. Additionally, the experimental kernel C verification API copied caller txdata on every call due to ternary expression typing, and `ConnectBlock()` allocated unused txdata memory during assumevalid verification.

Evidence of importance:

  • PR description: "The old Init() guard only checked m_spent_outputs_ready, which remained false after forced initialization without spent outputs even though force=true cached the BIP143 prevout, sequence, and output hashes for witness-v0 sighash."
  • Commit d07c727ff8: "Reproducer: https://godbolt.org/z/PzcKGW9da (GCC 15.2: one copy before, none after)."
  • PR description references closed PR #35663 ("validation: avoid preallocating unused txdata during assumevalid"), whose memory optimization is incorporated directly into ConnectBlock().

Scope: Touches 17 files, but most edits are mechanical adaptations across call sites (PSBT, wallet feebumper, fuzz targets, test fixtures) replacing two-step `PrecomputedTransactionData` initialization with direct construction or `std::optional` wrapping.

Risk of a mistake: medium (consensus, validation). A mistake in initializing or binding PrecomputedTransactionData could corrupt sighash computations during script execution, causing invalid transactions to be accepted or valid blocks to be rejected.

Reviewability

Ready · Needs rebase

Conflicts with master; review the logic now, the rebase is mechanical.

Waiting on (from the thread read):

  • reviewer: review of the updated stack and kernel API fix from theuni or other reviewers 2026-09-22

Agreement

Strong

substantive support, no open objection (fametrano)

Thread notes: Added sedited's suggestion to remove Init() and question on staleness, answered by fametrano who also provided a substantive Tested ACK. / Added sedited's suggestion to remove Init entirely and question regarding staleness. Added fametrano's tested ACK and answer to sedited's question.

Objections, suggestions, and questions:

ReviewerTypeHarm, request, or questionStatusMust beAuthor repliedQuote
sedited (member)objection, approachrelaxing the requirement against re-initialization and leaving a reuse path open instead of enforcing initialization at constructionfixed · fix pushed · Init was removed and initialization moved to construction, which sedited then ACKedchangedyes2026-07-08: 'We should never be hitting a code path where a re-initialization is possible, and this seems to relax that requirement. I think rather than messing with `Init`, we should seriously reconsider whether this data can be initialized at construction.'
Status from: 2026-07-09: l0rinc: 'Pushed: `PrecomputedTransactionData` is no longer default-constructible, public `Init()` is gone, and each object is built for one transaction.'
sedited (member)question, correctnesswhich staleness is specifically being prevented by this changeanswered · fametrano detailed how sequential Init calls across transactions retained cached BIP143 hashesansweredby others2026-09-22: 'Which staleness are we exactly preventing with this change?'
Status from: 2026-10-25: fametrano: 'On which staleness this prevents: on the base (7988380da9), `Init(A, {})` for a transaction with a witness, then `Init(B, spent_outputs)` for one without, passes the old `assert(!m_spent_outputs_ready)`'
sedited (member)suggestion, maintenanceremove Init() entirely rather than keeping it as a helperfixed · fix pushed · author removed Init() and moved its body directly into the constructoransweredyes2026-09-22: 'Why not remove it entirely? The diff from doing so is small.'
Status from: 2026-09-22: l0rinc: 'I removed `Init()` entirely and moved its body into the constructor - beautiful!'

Support:

  • sedited (member) (ACK, 2026-09-22): ACK on the changes [not substantive]
  • fametrano (contributor) (Tested ACK, 2026-10-25) [both: kernel API, libbitcoinkernel, src/script/interpreter.cpp]: differential sighash testing showed equivalence across 5961 BIP143 and BIP341 key paths, unit and kernel tests passed, and kernel API benchmark showed orders-of-magnitude speedup by eliminating txdata copying

Participants:

  • sedited (member, support, 3 statements 2026-07-08 to 2026-09-22): ACKed the changes; earlier objection and suggestion to eliminate Init() were adopted
  • fametrano (contributor, support, 1 statements 2026-10-25 to 2026-10-25): Tested ACK with sighash differential testing and kernel API benchmarks

Files

81 lines under test/bench/ci.

  • src/test/sighash_tests.cpp +37/-3
  • src/validation.cpp +14/-14
  • src/script/interpreter.cpp +4/-14
  • src/psbt.cpp +5/-9
  • src/test/txvalidationcache_tests.cpp +8/-6
  • src/kernel/bitcoinkernel.cpp +4/-8
  • src/script/interpreter.h +4/-8
  • src/script/sign.cpp +2/-5
  • src/test/fuzz/script_assets_test_minimizer.cpp +2/-4
  • src/test/script_assets_tests.cpp +2/-4
  • src/bench/verify_script.cpp +1/-2
  • src/signet.cpp +1/-2
  • src/test/descriptor_tests.cpp +1/-2
  • src/test/fuzz/script_flags.cpp +1/-2
  • src/test/fuzz/script_sigcache.cpp +2/-1
  • src/test/script_tests.cpp +1/-2
  • src/wallet/feebumper.cpp +1/-2

Card

The short text the ranking pass compares across PRs.

Removes default construction and the public Init() method from PrecomputedTransactionData, ensuring instances are immutably bound to a specific transaction and spent outputs upon construction. Validation and tests now hold an std::optional to defer construction until a script-cache miss, while ConnectBlock skips allocating txdata vectors entirely when assumevalid bypasses script verification. It also fixes an unintended copy of caller-supplied txdata on every call in the experimental bitcoinkernel C API, corroborated by a compiler reproducer.

Data

dossier JSON · extract JSON · model ledger, generated 2026-10-26T01:20, confidence high, input hash 42adb77d4a2e4518
thread read through 2026-10-25 (9 statements), head 35a8964f88
labels: Consensus, Needs rebase

Categories

Kernel (libbitcoinkernel)

P3 · speedup

Eliminating txdata copying in the experimental kernel C verification API resolves a significant performance issue, yielding orders-of-magnitude speedups in kernel API benchmarks.

Membership: src/kernel/bitcoinkernel.cpp is modified to eliminate an unintended copy of caller-supplied txdata in the kernel C verification API.

Factors: security/stability 1, bug 1, performance 1, user value 0, leverage 0

Validation

P3 · cleanup (ranking pass: Moved down: hardening of an error-prone pattern, no active bug, sits with the other defensive validation refactors)

The PR removes an error-prone stateful reinitialization pattern in PrecomputedTransactionData that allowed instances to be partially reinitialized or reused across transactions, and avoids allocating unused txdata during assumevalid block connection.

Membership: Labeled Consensus and modifies src/script/interpreter.{h,cpp} and src/validation.cpp.

Factors: security/stability 1, bug 1, performance 1, user value 0, leverage 0