{
 "number": 36386,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36386",
 "title": "compressor: oversized script decoding can leave a spendable script",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-09-30T01:25:49Z",
 "updated_at": "2026-10-31T18:21:52Z",
 "description_hash": "cfa3f44704470552",
 "age_days": 32,
 "draft": false,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
 "head_ref": "l0rinc/script-compression-replace-oversized",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-09-30T01:28:20Z",
   "sha": "6a30cea3cd313ca6a4b8178d129f62728e514c5f"
  },
  {
   "t": "2026-09-30T18:10:26Z",
   "sha": "f313faff443e5cdbd858a54c6bb939799d7ce714"
  },
  {
   "t": "2026-10-15T13:57:26Z",
   "sha": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd"
  }
 ],
 "additions": 17,
 "deletions": 1,
 "changed_files": 2,
 "commit_count": 2,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "fametrano",
      "url": "https://github.com/bitcoin/bitcoin/pull/36386#issuecomment-6396973053"
     },
     {
      "login": "hodlinator",
      "url": "https://github.com/bitcoin/bitcoin/pull/36386#pullrequestreview-5650692444"
     },
     {
      "login": "maflcko",
      "url": "https://github.com/bitcoin/bitcoin/pull/36386#issuecomment-6440879122"
     },
     {
      "login": "optout21",
      "url": "https://github.com/bitcoin/bitcoin/pull/36386#issuecomment-6444909520"
     }
    ],
    "stale_ack": [
     {
      "login": "jcastros",
      "url": "https://github.com/bitcoin/bitcoin/pull/36386#issuecomment-5997586155"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "jcastros": {
   "kind": "ack",
   "hash": "f313faff443e5cdbd858a54c6bb939799d7ce714",
   "t": "2026-10-05T15:27:09Z",
   "stale": true
  },
  "hodlinator": {
   "kind": "ack",
   "hash": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "t": "2026-10-26T10:45:52Z",
   "stale": false
  },
  "optout21": {
   "kind": "ack",
   "hash": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "t": "2026-10-27T14:40:12Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 2,
  "stale_ack": 1,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 1,
  "changes_requested": 0,
  "distinct_reviewers": [
   "fametrano",
   "hodlinator",
   "jcastros",
   "maflcko",
   "optout21"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-15T13:57:26Z",
  "last_reviewer_activity": "2026-10-27T14:40:12Z",
  "last_reviewer": "optout21",
  "author_silent_days": 17,
  "waiting_on_author_days": 5,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   36280
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 36280,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "validation: throw when a compressed script can't be decompressed"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/compressor.h",
  "src/test/compress_tests.cpp"
 ],
 "body": "**Problem:** The oversized-script fallback appends `OP_RETURN` to the destination instead of replacing it. If the destination already contains an unfinished push, the appended byte becomes pushed data and the result can remain spendable.\n\n**Fix:** Replace the destination with `OP_RETURN` rather than appending it.\n\nFound while reviewing #36280. This predates that PR and appears latent because oversized outputs are excluded from the UTXO set, but it could affect malformed stored records decoded into a reused destination.",
 "commits": [
  {
   "sha": "1c8ba894ed9c5d7aa39d94eec2de60f11488a76e",
   "date": "2026-10-15T13:53:46Z",
   "message": "test: characterize oversized script decoding into a reused target\n\nDecode an oversized script into a target containing an incomplete push.\nRecord that the resulting script fails IsUnspendable().\nThe following fix requires the decoded script to be unspendable."
  },
  {
   "sha": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "date": "2026-10-15T13:55:58Z",
   "message": "compressor: replace oversized scripts instead of appending\n\nThe oversized-script fallback appends OP_RETURN to the destination rather than replacing it as documented.\nA reused target retains its previous contents, and an incomplete push can consume OP_RETURN as data.\nAssign a new OP_RETURN script before skipping the oversized payload so the result is independent of the destination's prior contents."
  }
 ],
 "timeline": [
  {
   "t": "2026-09-30T01:28:20Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "6a30cea3cd313ca6a4b8178d129f62728e514c5f",
   "id": "push:6a30cea3cd313ca6a4b8178d129f62728e514c5f"
  },
  {
   "t": "2026-09-30T07:58:40Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nNot sure, reusing a destination would be an unrelated bug? So I am half suggesting to drop the test in a third commit again, or at least clarify in the comment that this isn't supposed to test any real production behavior.\n\nlgtm either way.",
   "id": "c:5906809717",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#issuecomment-5906809717",
   "hash": "ca7c4f1d9eebf6ab",
   "edited": null
  },
  {
   "t": "2026-09-30T18:10:26Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "f313faff443e5cdbd858a54c6bb939799d7ce714",
   "id": "push:f313faff443e5cdbd858a54c6bb939799d7ce714"
  },
  {
   "t": "2026-09-30T18:12:54Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nWe do have some existing reuses, e.g. in https://github.com/bitcoin/bitcoin/blob/e7aef7e86da79000aa42f5ba6d13b0d123d9da7c/src/rpc/blockchain.cpp#L3435-L3462 the snapshot export reuses the same `Coin` in the loop. Regardless, added a comment clarifying that this tests the fallback directly, since normal UTXO insertion excludes oversized scripts.",
   "id": "c:5917022886",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#issuecomment-5917022886",
   "hash": "a99e5868bc1a0602",
   "edited": null
  },
  {
   "t": "2026-10-05T15:27:09Z",
   "kind": "comment",
   "who": "jcastros",
   "assoc": "CONTRIBUTOR",
   "text": "Tested ACK f313faff443e5cdbd858a54c6bb939799d7ce714\n\nI built test_bitcoin (GCC 13.3, RelWithDebInfo) and ran compress_tests on this and on 11494efb01d0. Both are clean. Putting `script << OP_RETURN` back makes compress_oversized_script fail, so the new check covers the fix.",
   "id": "c:5997586155",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#issuecomment-5997586155",
   "hash": "0dfa2a88806339d6",
   "edited": null
  },
  {
   "t": "2026-10-14T09:36:39Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/compress_tests.cpp",
   "commit": "f313faff443e5cdbd858a54c6bb939799d7ce714",
   "in_reply_to": null,
   "text": "This has a bit too much happening at once IMO (+ redundant `CScript`-ctor-call). Would rather have something closer to\n```suggestion\n    DataStream oversized_compressed{DataStream{} << Using<ScriptCompression>(CScript{} << std::vector(MAX_SCRIPT_SIZE + 1, uint8_t{0}))};\n    oversized_compressed >> Using<ScriptCompression>(restored);\n```",
   "id": "rc:4269121804",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#discussion_r4269121804",
   "hash": "eb0d763540aa5cde",
   "edited": "2026-10-14T09:43:43Z"
  },
  {
   "t": "2026-10-14T09:39:00Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/compress_tests.cpp",
   "commit": "f313faff443e5cdbd858a54c6bb939799d7ce714",
   "in_reply_to": null,
   "text": "nit: Could verify that `restored` starts out spendable before the compression round-trip makes it unspendable?\n```suggestion\n    CScript restored{ParseScript(\"0x01\")}; // Incomplete push: appending OP_RETURN would make it pushed data\n    BOOST_CHECK(!restored.IsUnspendable());\n    (DataStream{} << Using<ScriptCompression>(CScript{CScript() << std::vector(MAX_SCRIPT_SIZE + 1, uint8_t{0})})) >> Using<ScriptCompression>(restored);\n```",
   "id": "rc:4269144356",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#discussion_r4269144356",
   "hash": "b4d5921442a83e78",
   "edited": "2026-10-14T09:43:43Z"
  },
  {
   "t": "2026-10-14T09:43:02Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/compress_tests.cpp",
   "commit": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "in_reply_to": null,
   "text": "Ralph suggested also pinning down the resulting script (https://git.fish.foo/bitcoin/bitcoin/pulls/36386#issuecomment-608275), which seems fair enough.\n\nGuess it could be done through:\n```suggestion\n    BOOST_CHECK(restored.IsUnspendable());\n    const CScript expected{OP_RETURN};\n    BOOST_CHECK_EQUAL_COLLECTIONS(restored.begin(), restored.end(), expected.begin(), expected.end());\n```",
   "id": "rc:4269186201",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#discussion_r4269186201",
   "hash": "5a4e22ae552fb290",
   "edited": "2026-10-14T09:43:43Z"
  },
  {
   "t": "2026-10-14T09:43:43Z",
   "kind": "review",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "f313faff443e5cdbd858a54c6bb939799d7ce714",
   "text": "Concept ACK f313faff443e5cdbd858a54c6bb939799d7ce714",
   "id": "r:5515914011",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#pullrequestreview-5515914011",
   "hash": "5eff56a778bd1c7e",
   "edited": null
  },
  {
   "t": "2026-10-15T13:57:26Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "id": "push:426f29c3f1639e4c97be63cdf4c2e76bb86b39dd"
  },
  {
   "t": "2026-10-25T11:19:22Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "NONE",
   "text": "tACK 426f29c3f1639e4c97be63cdf4c2e76bb86b39dd\n\nI decoded 1307 compressed scripts (raw sizes 0 to 39 and from just under `MAX_SCRIPT_SIZE` to 200000, truncated streams, every special type) into a fresh target and five reused ones, a P2PKH among them, and compared each result with what a library I maintain decodes from the same bytes. On the base, every complete oversized script decoded into a reused target keeps the old bytes and appends `OP_RETURN` (35 decodes). On this head every raw and oversized case matches for every target.\n\nOne path still depends on the target: an uncompressed P2PK (type 4 or 5) whose x is not on the curve leaves a reused target untouched, so a P2PKH stays a P2PKH. #36280 makes that path throw. Its `compressor.h` change merges cleanly with this one; only `compress_tests.cpp` conflicts. With both applied, no decode in the set depends on what the target held before. I can share the vectors and the test.",
   "id": "c:6396973053",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#issuecomment-6396973053",
   "hash": "109cb0908d157879",
   "edited": null
  },
  {
   "t": "2026-10-26T10:35:02Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/compress_tests.cpp",
   "commit": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "in_reply_to": null,
   "text": "nit: Could compute at compile time:\n```diff\n--- a/src/test/compress_tests.cpp\n+++ b/src/test/compress_tests.cpp\n@@ -28,6 +28,8 @@\n // amounts 50 .. 21000000\n #define NUM_MULTIPLES_50BTC 420000\n\n+using namespace util::hex_literals;\n+\n BOOST_FIXTURE_TEST_SUITE(compress_tests, BasicTestingSetup)\n\n bool static TestEncode(uint64_t in) {\n@@ -172,7 +174,7 @@ BOOST_AUTO_TEST_CASE(compress_p2pk_scripts_not_on_curve)\n BOOST_AUTO_TEST_CASE(compress_oversized_script)\n {\n     // Test the fallback directly, since normal UTXO insertion excludes oversized scripts\n-    CScript restored{ParseScript(\"0x01\")}; // Incomplete push: appending OP_RETURN would make it pushed data\n+    auto restored{CScript{} << \"01\"_hex}; // Incomplete push: appending OP_RETURN would make it pushed data\n     BOOST_CHECK(!restored.IsUnspendable());\n     DataStream oversized_compressed{DataStream{} << Using<ScriptCompression>(CScript{} << std::vector(MAX_SCRIPT_SIZE + 1, uint8_t{0}))};\n     oversized_compressed >> Using<ScriptCompression>(restored);\n```",
   "id": "rc:4376274486",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#discussion_r4376274486",
   "hash": "f0120c39d727e472",
   "edited": "2026-10-26T10:45:52Z"
  },
  {
   "t": "2026-10-26T10:45:52Z",
   "kind": "review",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "text": "ACK 426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "id": "r:5650692444",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#pullrequestreview-5650692444",
   "hash": "3db45a42bccba877",
   "edited": null
  },
  {
   "t": "2026-10-27T11:34:28Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "review ACK 426f29c3f1639e4c97be63cdf4c2e76bb86b39dd \ud83c\udfc9\n\nShow signature\n\nSignature:\n\n```\nuntrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm \"${path_to_any_empty_file}\" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x \"${path_to_this_whole_four_line_signature_blob}\"\nRUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=\ntrusted comment: review ACK 426f29c3f1639e4c97be63cdf4c2e76bb86b39dd \ud83c\udfc9\nZMFrq8y3VWJkvdmEHZqxUEXnr07PVirQi5JeVZZ0cI9PWixMNhtsYF/RYb7eALrDfdaoDa4OPwLQP3GmubegDg==\n```",
   "id": "c:6440879122",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#issuecomment-6440879122",
   "hash": "c1f6f33e00d73123",
   "edited": null
  },
  {
   "t": "2026-10-27T13:57:47Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/compressor.h",
   "commit": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThe commit message states: \"so the result is independent of the destination's prior contents\". However, this is not always the case. In this branch, `DecompressScript` could fail in such a way that it does not modify `script` (in the `0x04`/`0x05` prefix + error case).\nEither adjust this codepath as well (reset it in case of error), or soften the claim.",
   "id": "rc:4389555092",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#discussion_r4389555092",
   "hash": "b79932b22f6c4c7d",
   "edited": "2026-10-27T14:16:42Z"
  },
  {
   "t": "2026-10-27T14:15:15Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/compressor.h",
   "commit": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nWould it be possible to clear the `script` output variable at the beginning of this method? That would ensure the claim (independence of previous value).",
   "id": "rc:4389749344",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#discussion_r4389749344",
   "hash": "219159f1e636dbc1",
   "edited": "2026-10-27T14:16:42Z"
  },
  {
   "t": "2026-10-27T14:16:42Z",
   "kind": "review",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "text": "Reviewed, left some comments. Happy to re-check/ack once answered.\n(426f29c3f1639e4c97be63cdf4c2e76bb86b39dd)",
   "id": "r:5668222358",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#pullrequestreview-5668222358",
   "hash": "4028d1d337da7e54",
   "edited": null
  },
  {
   "t": "2026-10-27T14:20:26Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/compressor.h",
   "commit": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
   "in_reply_to": 4389555092,
   "text": "I've just realized that #36280 addresses exactly this, so I think it's fine as it is in this PR.",
   "id": "rc:4389807813",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#discussion_r4389807813",
   "hash": "c135e409e4b7c042",
   "edited": "2026-10-27T14:20:57Z"
  },
  {
   "t": "2026-10-27T14:40:12Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "ACK 426f29c3f1639e4c97be63cdf4c2e76bb86b39dd\nUpgraded my previous review result, upon reconsideration. This change is a net improvement, independent of #36280 . I'd appreciate answers to my comments, nonetheless.",
   "id": "c:6444909520",
   "url": "https://github.com/bitcoin/bitcoin/pull/36386#issuecomment-6444909520",
   "hash": "03fa8284ac3dc4cc",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-30T01:29:31Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-30T02:20:52Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 6951,
 "text_tokens_estimate": 1737,
 "changed_paths": [
  "src/compressor.h",
  "src/test/compress_tests.cpp"
 ],
 "files": [
  {
   "path": "src/compressor.h",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/compress_tests.cpp",
   "add": 16,
   "del": 0
  }
 ],
 "test_lines": 16,
 "git": {
  "head": "426f29c3f1639e4c97be63cdf4c2e76bb86b39dd",
  "head_matches_backup": true,
  "base": "cce32b16874e85e8a00cfa1a2190f0b6f4dcaf66",
  "patch_id": "93b578d8b06c8d01",
  "commits": [
   {
    "sha": "1c8ba894ed",
    "subject": "test: characterize oversized script decoding into a reused target",
    "files": 1,
    "add": 15,
    "del": 0
   },
   {
    "sha": "426f29c3f1",
    "subject": "compressor: replace oversized scripts instead of appending",
    "files": 2,
    "add": 3,
    "del": 2
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "93b578d8b06c8d01",
 "input_hash": "562d038df93c6252",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}