{
 "number": 35662,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35662",
 "title": "script: prevent stale sighash caches across transactions",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-07-06T01:58:55Z",
 "updated_at": "2026-09-10T13:59:36Z",
 "age_days": 73,
 "draft": false,
 "labels": [
  "Consensus"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "a5ac29bfb6c1d8eba1999559dad806d0977963fb",
 "head_ref": "l0rinc/script-reset-precomputed-txdata",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-07-09T05:54:05Z",
   "sha": "586620e3878ac65e8ed4ded89e0725d2afab454c"
  },
  {
   "t": "2026-07-09T19:21:42Z",
   "sha": "92470f76bd8db83b304d4f79586a1803cd794e4f"
  },
  {
   "t": "2026-08-05T22:27:06Z",
   "sha": "a5ac29bfb6c1d8eba1999559dad806d0977963fb"
  }
 ],
 "additions": 82,
 "deletions": 74,
 "changed_files": 17,
 "commit_count": 4,
 "size_bucket": "M",
 "mergeable_state": "unknown",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": [
    {
     "number": 35713,
     "title": "Remove boost as a unit test runner",
     "author": "rustaceanrob"
    },
    {
     "number": 35569,
     "title": "Encapsulation for CTransaction",
     "author": "purpleKarrot"
    },
    {
     "number": 35511,
     "title": "RFC: consensus: Make `CAmount` a class",
     "author": "hodlinator"
    },
    {
     "number": 32575,
     "title": "consensus: Remove special treatment for single threaded script checking",
     "author": "fjahr"
    },
    {
     "number": 29843,
     "title": "policy: Allow non-standard scripts with -acceptnonstdtxn=1 (test nets only)",
     "author": "ajtowns"
    },
    {
     "number": 29491,
     "title": "[EXPERIMENTAL] Schnorr batch verification for blocks",
     "author": "fjahr"
    }
   ]
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "sedited"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "unknown",
  "last_author_activity": "2026-08-05T22:29:08Z",
  "last_reviewer_activity": "2026-07-08T08:41:05Z",
  "last_reviewer": "sedited",
  "author_silent_days": 42,
  "waiting_on_author_days": 0,
  "days_since_update": 7
 },
 "refs": {
  "mentioned": [
   35663
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35663,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "validation: avoid preallocating unused txdata during assumevalid"
   }
  ],
  "conflicts": [
   35713,
   35569,
   35511,
   32575,
   29843,
   29491
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/script/interpreter.cpp"
 ],
 "body": "**Problem:** `PrecomputedTransactionData` could be default-constructed and filled later with `Init()`.\nThat allowed an object to exist without transaction-specific data and left a public reuse path that was easy to get wrong.\nThe old `Init()` guard only checked [`m_spent_outputs_ready`](https://github.com/bitcoin/bitcoin/blob/2063f02bd5edebe1b5c9635db8850220811ebc90/src/script/interpreter.cpp#L1415), 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.\n\n**Fix:** Remove default construction and public `Init()` so each `PrecomputedTransactionData` object is built for one transaction.\nCallers that already have the transaction and optional spent outputs construct it directly.\nValidation stores txdata in `std::optional` and emplaces it only after a script-cache miss, keeping precomputation lazy.\n`ConnectBlock()` also leaves txdata storage empty when assumevalid skips script checks, covering #35663.",
 "commits": [
  {
   "sha": "3dd197083140ad83f1852e8e6ff78c229b6fe99a",
   "date": "2026-08-05T21:29:34Z",
   "message": "script: construct txdata at simple call sites\n\nExtend the transaction-only constructor to accept spent outputs and a `force` flag, then use it where those inputs are already available.\nThis preserves each caller's precomputation behavior while removing default construction from the simple cases."
  },
  {
   "sha": "df7ed36b0410f0bfe31915e7c890edf0ba2283a3",
   "date": "2026-08-05T22:25:07Z",
   "message": "script: construct txdata at other eager callers\n\n`SignTransaction()` can provide spent outputs only when every input coin is available, so discard a partial collection when one is missing and construct txdata once with forced precomputation.\nThe `script_sigcache` fuzz target exercises signature-cache methods that do not read txdata, so construct it from an empty transaction to avoid per-iteration hashing."
  },
  {
   "sha": "d31ab5f47f60ddef88999b45da582d89f7314351",
   "date": "2026-08-05T22:25:14Z",
   "message": "validation: defer txdata construction\n\nScript-cache hits return before validation gathers spent outputs or precomputes transaction data.\nStore txdata in `std::optional` and emplace it only after a cache miss to preserve that behavior.\n`ConnectBlock()` can skip script checks for blocks in the assumevalid period, so size its txdata storage only when checks can run."
  },
  {
   "sha": "a5ac29bfb6c1d8eba1999559dad806d0977963fb",
   "date": "2026-08-05T22:25:14Z",
   "message": "script: make txdata construction mandatory\n\nRemove default construction and public `Init()` so each `PrecomputedTransactionData` object is built for one transaction.\nKeep `Init()` as the private shared implementation and cover the supported constructor states."
  }
 ],
 "timeline": [
  {
   "t": "2026-07-08T08:41:05Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/script/interpreter.cpp",
   "commit": "1279d5d95cdc11b00909df51cc5d8181fa14d320",
   "in_reply_to": null,
   "text": "I'm tending NACK here. Afaict this is just a hardening improvement, but I'm not sure if it really is one. 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."
  },
  {
   "t": "2026-07-09T00:51:37Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/script/interpreter.cpp",
   "commit": "1279d5d95cdc11b00909df51cc5d8181fa14d320",
   "in_reply_to": 3542478315,
   "text": "I agree that resetting readiness flags inside `Init()` still leaves a reuse API around and makes the type responsible for defending against misuse after construction (I was hoping someone would object).\n\nI\u2019ll rework this so that `PrecomputedTransactionData` is always built for one transaction and has no public reinitialization path:\n* Call sites that already have the transaction and spent outputs can construct it directly.\n* Call sites that need to wait, such as validation before the script execution cache check, can store `std::optional<PrecomputedTransactionData>` and `emplace()` once all required data has been gathered.\n\nThis also seems like a natural place to fold in #35663: validation already needs optional txdata storage, so the assumevalid path can leave the per-block txdata vector empty when script checks are skipped, while still resizing it before any queued checks can take pointers into it."
  },
  {
   "t": "2026-07-09T05:54:05Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "586620e3878ac65e8ed4ded89e0725d2afab454c"
  },
  {
   "t": "2026-07-09T06:05:51Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/script/interpreter.cpp",
   "commit": "1279d5d95cdc11b00909df51cc5d8181fa14d320",
   "in_reply_to": 3542478315,
   "text": "Pushed: `PrecomputedTransactionData` is no longer default-constructible, public `Init()` is gone, and each object is built for one transaction.\n`ConnectBlock()` also leaves txdata storage empty when script checks are skipped during assumevalid, so that path avoids the unused vector allocation.\nThis is indeed a lot nicer, thanks for the push. What do you think?"
  },
  {
   "t": "2026-07-09T19:21:42Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "92470f76bd8db83b304d4f79586a1803cd794e4f"
  },
  {
   "t": "2026-08-05T22:27:06Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "a5ac29bfb6c1d8eba1999559dad806d0977963fb"
  },
  {
   "t": "2026-08-05T22:29:08Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Rebased after recent merges, ready for review again. I understood @theuni has a similar attempt before, if it's published somewhere, please let me know and I'll add him as a coauthor."
  }
 ],
 "labels_log": [
  {
   "t": "2026-07-06T01:58:57Z",
   "action": "labeled",
   "label": "Consensus",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-09T06:58:27Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-09T20:26:42Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-04T22:55:22Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-05T22:40:42Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-07-08T23:56:50Z",
   "kind": "convert_to_draft",
   "who": "l0rinc"
  },
  {
   "t": "2026-07-09T05:57:18Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "script: reset precomputed txdata on reuse",
   "to": "script: construct txdata at initialization sites"
  },
  {
   "t": "2026-07-09T06:00:21Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "script: construct txdata at initialization sites",
   "to": "script: remove deferred txdata initialization"
  },
  {
   "t": "2026-07-09T06:02:19Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "script: remove deferred txdata initialization",
   "to": "script: make txdata non-default-constructible"
  },
  {
   "t": "2026-07-09T19:22:41Z",
   "kind": "ready_for_review",
   "who": "l0rinc"
  },
  {
   "t": "2026-08-05T22:27:22Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "script: make txdata non-default-constructible",
   "to": "script: prevent stale sighash caches across transactions"
  }
 ],
 "text_chars": 4233,
 "text_tokens_estimate": 1058,
 "changed_paths": [
  "src/bench/verify_script.cpp",
  "src/kernel/bitcoinkernel.cpp",
  "src/psbt.cpp",
  "src/script/interpreter.cpp",
  "src/script/interpreter.h",
  "src/script/sign.cpp",
  "src/signet.cpp",
  "src/test/descriptor_tests.cpp",
  "src/test/fuzz/script_assets_test_minimizer.cpp",
  "src/test/fuzz/script_flags.cpp",
  "src/test/fuzz/script_sigcache.cpp",
  "src/test/script_assets_tests.cpp",
  "src/test/script_tests.cpp",
  "src/test/sighash_tests.cpp",
  "src/test/txvalidationcache_tests.cpp",
  "src/validation.cpp",
  "src/wallet/feebumper.cpp"
 ],
 "files": [
  {
   "path": "src/bench/verify_script.cpp",
   "add": 1,
   "del": 2
  },
  {
   "path": "src/kernel/bitcoinkernel.cpp",
   "add": 2,
   "del": 7
  },
  {
   "path": "src/psbt.cpp",
   "add": 5,
   "del": 9
  },
  {
   "path": "src/script/interpreter.cpp",
   "add": 4,
   "del": 4
  },
  {
   "path": "src/script/interpreter.h",
   "add": 4,
   "del": 5
  },
  {
   "path": "src/script/sign.cpp",
   "add": 2,
   "del": 5
  },
  {
   "path": "src/signet.cpp",
   "add": 1,
   "del": 2
  },
  {
   "path": "src/test/descriptor_tests.cpp",
   "add": 1,
   "del": 2
  },
  {
   "path": "src/test/fuzz/script_assets_test_minimizer.cpp",
   "add": 2,
   "del": 4
  },
  {
   "path": "src/test/fuzz/script_flags.cpp",
   "add": 1,
   "del": 2
  },
  {
   "path": "src/test/fuzz/script_sigcache.cpp",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/test/script_assets_tests.cpp",
   "add": 2,
   "del": 4
  },
  {
   "path": "src/test/script_tests.cpp",
   "add": 1,
   "del": 2
  },
  {
   "path": "src/test/sighash_tests.cpp",
   "add": 30,
   "del": 3
  },
  {
   "path": "src/test/txvalidationcache_tests.cpp",
   "add": 8,
   "del": 6
  },
  {
   "path": "src/validation.cpp",
   "add": 15,
   "del": 14
  },
  {
   "path": "src/wallet/feebumper.cpp",
   "add": 1,
   "del": 2
  }
 ],
 "test_lines": 74,
 "git": {
  "head": "a5ac29bfb6c1d8eba1999559dad806d0977963fb",
  "head_matches_backup": true,
  "base": "c4fbd3c72114e3466329ec6f4b0f0a4b7ebce3b1",
  "commits": [
   {
    "sha": "3dd1970831",
    "subject": "script: construct txdata at simple call sites",
    "files": 11,
    "add": 20,
    "del": 34
   },
   {
    "sha": "df7ed36b04",
    "subject": "script: construct txdata at other eager callers",
    "files": 3,
    "add": 6,
    "del": 13
   },
   {
    "sha": "d31ab5f47f",
    "subject": "validation: defer txdata construction",
    "files": 2,
    "add": 23,
    "del": 20
   },
   {
    "sha": "a5ac29bfb6",
    "subject": "script: make txdata construction mandatory",
    "files": 2,
    "add": 34,
    "del": 8
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "263060b1340aa57a",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}