{
 "number": 36395,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36395",
 "title": "script: clarify implicit signature-cache input requirements",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-09-30T22:39:18Z",
 "updated_at": "2026-10-31T17:01:53Z",
 "description_hash": "358f55913bd71900",
 "age_days": 32,
 "draft": false,
 "labels": [
  "Consensus"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "c8d6a97afb366f82680601713942afb4955c2885",
 "head_ref": "l0rinc/sigcache-input-invariants",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-10-01T05:22:49Z",
   "sha": "c8d6a97afb366f82680601713942afb4955c2885"
  }
 ],
 "additions": 152,
 "deletions": 39,
 "changed_files": 7,
 "commit_count": 9,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "instagibbs",
      "url": "https://github.com/bitcoin/bitcoin/pull/36395#issuecomment-5939526401"
     },
     {
      "login": "fametrano",
      "url": "https://github.com/bitcoin/bitcoin/pull/36395#issuecomment-6397188720"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "instagibbs": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-01T20:03:27Z",
   "stale": false
  },
  "fametrano": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-25T11:42:10Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 2,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "fametrano",
   "instagibbs"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-02T03:38:52Z",
  "last_reviewer_activity": "2026-10-25T11:42:10Z",
  "last_reviewer": "fametrano",
  "author_silent_days": 30,
  "waiting_on_author_days": 7,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "**Problem:** Signature-cache hashing relies on input requirements that are implicit at its public entry methods.\nInspired by the Elements rangeproof-cache vulnerability behind the recent [Liquid incident](https://blog.blockstream.com/liquid-network-security-incident-assessment) and the [subsequent cache-key fix](https://github.com/ElementsProject/elements/pull/1600), this change clarifies an easy-to-misread hashing pattern: `Write(data, size)` hashes the data bytes without encoding the size.\nBitcoin Core's cache keys already include the signature hash, public key, and signature in separate ECDSA and Schnorr domains.\nBitcoin Core's signature-cache encoding is unambiguous for inputs accepted by its current script checker: a valid ECDSA public key's prefix determines its length, while a Schnorr public key is always 32 bytes, leaving the signature as the remaining suffix.\n\n**Fix:** Extract the common hashing into one typed helper so changes to the ECDSA and Schnorr layouts stay synchronized.\nReturn the digest by value from the helper and public `ComputeEntry*` methods so callers explicitly initialize the cache entry, where the previous chained `Finalize(entry.begin())` obscured the output assignment.\nDocument the existing layout and assert the valid ECDSA public-key encoding and 64-byte Schnorr signature requirements.\nThe serialized bytes and script validity rules are unchanged.",
 "commits": [
  {
   "sha": "74c70daf6b468d8cf98b70829e41f05462fdca63",
   "date": "2026-09-30T22:13:01Z",
   "message": "test: precompute script transaction data\n\nPrecompute transaction data for the existing script vectors so plain and caching verification can share the same immutable transactions.\nUse `uint32_t` for BIP341 input positions to match the checker interface."
  },
  {
   "sha": "1339ded9308ce7307f570f42a3d453afe11b8cc9",
   "date": "2026-09-30T22:13:01Z",
   "message": "test: compare plain and cached verification\n\nRequire caching to preserve the existing script vectors' results and errors and the fuzz inputs' verification results.\nRepeat script verification after successful signatures are stored, and alternate plain and caching checkers under random flag combinations.\nAllow empty ECDSA signatures, which can remain after removing the sighash byte, and require valid public-key encodings for direct calls that bypass the script checker."
  },
  {
   "sha": "25156225ce6848211b434f653f80c3ca4d940ebb",
   "date": "2026-10-01T05:12:23Z",
   "message": "test: cover public-key encoding lengths\n\nExtend `pubkey_unserialize` to check every header and lengths through one byte beyond the largest key.\nRequire construction and deserialization to agree, and round-trip each resulting key without unread bytes."
  },
  {
   "sha": "498bf7d2c950516aa2fb6dfdaaa1cc729634938e",
   "date": "2026-10-01T05:12:23Z",
   "message": "test: cover malformed ECDSA public keys\n\nExtend an existing lax-encoding vector with a non-empty, wrong-length public key to exercise rejection by the script checker."
  },
  {
   "sha": "278154f849875c6cad1eb0e9138faab079af3eb0",
   "date": "2026-10-01T05:12:23Z",
   "message": "test: cover ECDSA signatures padded past 255 bytes\n\nLax DER verification accepts trailing data, including signatures longer than 255 bytes.\nAdd a padded P2PK vector to exercise that accepted input through both plain and caching verification."
  },
  {
   "sha": "92cb7affd5113f68c8deb3c577ff3e8157706d93",
   "date": "2026-10-01T05:12:24Z",
   "message": "test: cover Schnorr cache inputs\n\nExtend the BIP341 vectors to check insertion, non-storing hits, entries that distinguish public keys and sighashes, and rejection without caching of a correctly sized invalid signature.\nCover both 64- and 65-byte script signatures and the exact errors for malformed sizes and an explicit default sighash byte."
  },
  {
   "sha": "670ce0d7a5f2bd5ef7fff62c68c0406c9fd328de",
   "date": "2026-10-01T05:12:24Z",
   "message": "refactor: return signature-cache entries by value\n\nReturn entries through the public `ComputeEntry*` methods so verification sites explicitly initialize them.\nThis replaces output parameters whose assignment was hidden in the chained `Finalize(entry.begin())`.\nThe hashing procedure and serialized bytes are unchanged."
  },
  {
   "sha": "4d5c4d36e97310f19cbb12f821bb71f066be1878",
   "date": "2026-10-01T05:12:24Z",
   "message": "refactor: extract and deduplicate sigcache hashing\n\nExtract the common hashing so ECDSA and Schnorr serialization changes stay synchronized.\nConstrain `PubKey` to `CPubKey` or `XOnlyPubKey` so swapping the signature and public-key arguments cannot compile.\nThe salted hash states, field order, and serialized bytes are unchanged."
  },
  {
   "sha": "c8d6a97afb366f82680601713942afb4955c2885",
   "date": "2026-10-01T05:12:24Z",
   "message": "script: assert signature-cache input contracts\n\nAssert and document the script checker's input guarantees before hashing cache entries, including direct calls to the caching verifier.\n`ComputeEntrySchnorr` is also called directly, so it enforces the 64-byte contract independently of the assertion in `XOnlyPubKey::VerifySchnorr`."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-01T05:22:49Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "c8d6a97afb366f82680601713942afb4955c2885",
   "id": "push:c8d6a97afb366f82680601713942afb4955c2885"
  },
  {
   "t": "2026-10-01T20:03:27Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "concept ACK on the tests, concept NACKy on the refactors due to blast radius. Will review the tests closer if split out",
   "id": "c:5939526401",
   "url": "https://github.com/bitcoin/bitcoin/pull/36395#issuecomment-5939526401",
   "hash": "733aa6877de6e579",
   "edited": null
  },
  {
   "t": "2026-10-01T21:48:23Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "NONE",
   "text": "I ran the two script_tests.json rows this adds or changes, at c8d6a97afb366f82680601713942afb4955c2885, through btclib's script interpreter, and both give the expected result. I haven't reviewed the sigcache changes.",
   "id": "c:5941347418",
   "url": "https://github.com/bitcoin/bitcoin/pull/36395#issuecomment-5941347418",
   "hash": "55205f7618730858",
   "edited": null
  },
  {
   "t": "2026-10-02T03:38:52Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThe refactor is the meat of this change, the original error would still be possible even if we only add tests, and I want to make sure we learn from those mistakes and refactor to make such misunderstandings harder.\nIf we're afraid of touching a part of the code, it means it's not ours yet, and it's probably where the ugly bugs are buried.",
   "id": "c:5945164869",
   "url": "https://github.com/bitcoin/bitcoin/pull/36395#issuecomment-5945164869",
   "hash": "d502131880d4fdae",
   "edited": null
  },
  {
   "t": "2026-10-25T11:42:10Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "NONE",
   "text": "Concept ACK\n\nThe new tests catch five signature-cache bugs that the current unit tests miss. I made seven one-line breaking changes to `sigcache.cpp` and ran the unit tests on c8d6a97afb36 and on its base, d4b0e1e4b0.\n\nOn the base, the unit tests still pass with six of the seven changes. Only storing failed Schnorr checks is caught, by `txvalidationcache_tests`. On this PR, `script_tests` catches five of those six: dropping the public key or the sighash from the ECDSA or the Schnorr entry, and storing failed ECDSA checks. The cached passes in `DoTest` catch the ECDSA ones, and the new BIP341 vector checks catch the Schnorr ones.\n\nThe sixth change gives Schnorr entries the ECDSA salt, and no unit test catches it on either commit. The separate salts are all that keeps the two domains apart: with one salt, the ECDSA entry for a 33-byte key and a 63-byte signature equals the Schnorr entry for the key's first 32 bytes and a signature made of the key's last byte followed by those 63 bytes. A test that builds both entries with `ComputeEntryECDSA` and `ComputeEntrySchnorr` and checks that they differ passes on this PR and fails with that change. It could go in this PR; I can share it, and the seven changes.",
   "id": "c:6397188720",
   "url": "https://github.com/bitcoin/bitcoin/pull/36395#issuecomment-6397188720",
   "hash": "14e2c76f9a4f3ea7",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-30T22:39:22Z",
   "action": "labeled",
   "label": "Consensus",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-30T23:40:58Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-01T07:41:13Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-10-01T06:39:16Z",
   "kind": "closed",
   "who": "l0rinc"
  },
  {
   "t": "2026-10-01T06:39:20Z",
   "kind": "reopened",
   "who": "l0rinc"
  }
 ],
 "text_chars": 6087,
 "text_tokens_estimate": 1521,
 "changed_paths": [
  "src/script/interpreter.h",
  "src/script/sigcache.cpp",
  "src/script/sigcache.h",
  "src/test/data/script_tests.json",
  "src/test/fuzz/script_sigcache.cpp",
  "src/test/key_tests.cpp",
  "src/test/script_tests.cpp"
 ],
 "files": [
  {
   "path": "src/script/interpreter.h",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/script/sigcache.cpp",
   "add": 25,
   "del": 10
  },
  {
   "path": "src/script/sigcache.h",
   "add": 8,
   "del": 3
  },
  {
   "path": "src/test/data/script_tests.json",
   "add": 8,
   "del": 1
  },
  {
   "path": "src/test/fuzz/script_sigcache.cpp",
   "add": 6,
   "del": 5
  },
  {
   "path": "src/test/key_tests.cpp",
   "add": 15,
   "del": 11
  },
  {
   "path": "src/test/script_tests.cpp",
   "add": 88,
   "del": 9
  }
 ],
 "test_lines": 143,
 "git": {
  "head": "c8d6a97afb366f82680601713942afb4955c2885",
  "head_matches_backup": true,
  "base": "d4b0e1e4b078dbb4ab57886d288470684dec5718",
  "patch_id": "406347618b7e7d57",
  "commits": [
   {
    "sha": "74c70daf6b",
    "subject": "test: precompute script transaction data",
    "files": 1,
    "add": 10,
    "del": 5
   },
   {
    "sha": "1339ded930",
    "subject": "test: compare plain and cached verification",
    "files": 2,
    "add": 24,
    "del": 10
   },
   {
    "sha": "25156225ce",
    "subject": "test: cover public-key encoding lengths",
    "files": 1,
    "add": 15,
    "del": 11
   },
   {
    "sha": "498bf7d2c9",
    "subject": "test: cover malformed ECDSA public keys",
    "files": 1,
    "add": 1,
    "del": 1
   },
   {
    "sha": "278154f849",
    "subject": "test: cover ECDSA signatures padded past 255 bytes",
    "files": 2,
    "add": 11,
    "del": 0
   },
   {
    "sha": "92cb7affd5",
    "subject": "test: cover Schnorr cache inputs",
    "files": 1,
    "add": 61,
    "del": 1
   },
   {
    "sha": "670ce0d7a5",
    "subject": "refactor: return signature-cache entries by value",
    "files": 3,
    "add": 12,
    "del": 13
   },
   {
    "sha": "4d5c4d36e9",
    "subject": "refactor: extract and deduplicate sigcache hashing",
    "files": 2,
    "add": 21,
    "del": 9
   },
   {
    "sha": "c8d6a97afb",
    "subject": "script: assert signature-cache input contracts",
    "files": 3,
    "add": 8,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "406347618b7e7d57",
 "input_hash": "7343d5071a62343a",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}