{
 "number": 36543,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36543",
 "title": "validation: check the most recent AssumeUTXO commitment during IBD",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-10-15T13:40:24Z",
 "updated_at": "2026-10-30T23:02:56Z",
 "description_hash": "51ccf018f787926c",
 "age_days": 17,
 "draft": false,
 "labels": [
  "Validation"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "97b2138e39ce34e239039e7d38d95e15f3c19a9d",
 "head_ref": "l0rinc/assumeutxo-latest-snapshot",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-10-15T13:59:28Z",
   "sha": "97b2138e39ce34e239039e7d38d95e15f3c19a9d"
  }
 ],
 "additions": 216,
 "deletions": 220,
 "changed_files": 17,
 "commit_count": 7,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "Sjors",
      "url": "https://github.com/bitcoin/bitcoin/pull/36543#issuecomment-6229005853"
     },
     {
      "login": "optout21",
      "url": "https://github.com/bitcoin/bitcoin/pull/36543#issuecomment-6230060049"
     },
     {
      "login": "fametrano",
      "url": "https://github.com/bitcoin/bitcoin/pull/36543#pullrequestreview-5643008477"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "Sjors": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-17T03:24:28Z",
   "stale": false
  },
  "optout21": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-17T05:16:28Z",
   "stale": false
  },
  "fametrano": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-25T11:38:48Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 3,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Sjors",
   "ajtowns",
   "fametrano",
   "optout21"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-15T13:59:28Z",
  "last_reviewer_activity": "2026-10-25T11:38:48Z",
  "last_reviewer": "fametrano",
  "author_silent_days": 17,
  "waiting_on_author_days": 7,
  "days_since_update": 2
 },
 "refs": {
  "mentioned": [
   32377
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 32377,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "AssumeUTXO: crowdsource trust through independent snapshot verification"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "**Problem:** AssumeUTXO verifies imported UTXOs against a hash we bake into the binary.\nEvery node reconstructs the same UTXO set during ordinary IBD, but [does not check its hash against the existing AssumeUTXO commitment](https://github.com/bitcoin/bitcoin/issues/32377).\n\n**Fix:** During ordinary block validation, verify the provided AssumeUTXO hash when block connection reaches the corresponding block, stopping the node on a mismatch.\nFlush pending changes to disk before hashing the UTXO set and comparing the result with the commitment.\n\nKeep only the latest AssumeUTXO commitment per network to limit the extra hashing work during IBD, and update the release process to replace it.\nUsers who need older snapshots for research or other purposes can use a compatible older release or restore the corresponding commitment as `m_assumeutxo_data` and recompile.",
 "commits": [
  {
   "sha": "1606010c94f76bf536acaddfe90b03d39b6a7645",
   "date": "2026-10-15T13:16:22Z",
   "message": "test: move AssumeUTXO commitments into test fixtures\n\nMove the height-110 and height-200 regtest commitments into their unit and fuzz fixtures.\nKeep the immutable fixture parameters alive across chainstate-manager restarts.\nLeave only the height-299 functional-test commitment in the default regtest parameters.\nPersist block-index test edits before reloading to prepare for the IBD check's forced flush.\nCheck that an unrelated chain passes the fixture's commitment height without starting a UTXO scan.\nCheck that interrupted snapshot validation leaves the snapshot unvalidated and can be retried."
  },
  {
   "sha": "59eeb531b0d6b9503f2c070852ab651f73a534c3",
   "date": "2026-10-15T13:16:22Z",
   "message": "chainparams: use the latest AssumeUTXO commitment\n\nMatch AssumeUTXO imports, background validation, and chainstate reloads against the latest commitment.\nKeep the height and block hash mismatch checks, and report the supported height for an unknown snapshot block hash.\nOlder AssumeUTXO commitments are no longer recognized.\nAll configured commitment lists are nonempty and ordered by increasing height, so the last entry supplies the same height as the previous max_element lookup for rollback exports.\nUse the same accessor for the fixture checks and remove the optional lookup APIs and height list."
  },
  {
   "sha": "9646f56002bd5868479df34a93dbd7755d0b1303",
   "date": "2026-10-15T13:16:22Z",
   "message": "refactor: extract AssumeUTXO hash verification\n\nExtract UTXO hashing, interruption handling, commitment comparison, and result logging from AssumeUTXO background validation.\nKeep snapshot invalidation at the caller so ordinary block validation can reuse the check.\nUse log wording that also fits ordinary block validation."
  },
  {
   "sha": "27705e415f1f9e5e18844509aa9a4ce29e5c2fcd",
   "date": "2026-10-15T13:59:20Z",
   "message": "refactor: store one AssumeUTXO commitment\n\nAll consumers now use only the latest commitment.\nRetain only that commitment to limit the extra hashing work during IBD.\nRemove the unused older production commitments and replace the vector with one optional `AssumeutxoData`.\nKeep it disengaged until the network constructor assigns the commitment."
  },
  {
   "sha": "36a2a70452b97b26794044e62db0a25010ca9fa4",
   "date": "2026-10-15T13:59:20Z",
   "message": "validation: verify the provided AssumeUTXO hash\n\nVerify the provided AssumeUTXO hash when block connection reaches the corresponding block.\nSkip other chains at the same height.\nThis checks the developer-provided commitment against the UTXO set that every node reconstructs during IBD (see #32377).\nReuse the AssumeUTXO background verifier and its reporting.\nFlush pending UTXO changes to disk before hashing the set.\nStop on a flush failure before updating the tip or mempool.\nRequest shutdown if UTXO statistics cannot be computed or the commitment does not match, preserving normal shutdown interruptions.\nAssumeUTXO import verification and background validation remain mandatory."
  },
  {
   "sha": "c296123d4dff21ea0632636be81b0ad218d6538f",
   "date": "2026-10-15T13:59:20Z",
   "message": "validation: propagate snapshot verification errors\n\n`MaybeValidateSnapshot()` already invalidates a failed snapshot and requests shutdown, but `ConnectTip()` reports success to its caller.\nReturn a runtime error for snapshot-validation failures without marking the connected block invalid.\nKeep interrupted hashing distinct so normal shutdown does not become a validation failure."
  },
  {
   "sha": "97b2138e39ce34e239039e7d38d95e15f3c19a9d",
   "date": "2026-10-15T13:59:20Z",
   "message": "doc: describe latest AssumeUTXO commitment checks\n\nDocument hash verification at the corresponding block and shutdown on a mismatch.\nExplain the compatibility change and how to use older snapshots for research.\nUpdate the release instructions to replace the commitment instead of appending it."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-15T13:59:28Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "97b2138e39ce34e239039e7d38d95e15f3c19a9d",
   "id": "push:97b2138e39ce34e239039e7d38d95e15f3c19a9d"
  },
  {
   "t": "2026-10-17T03:24:28Z",
   "kind": "comment",
   "who": "Sjors",
   "assoc": "MEMBER",
   "text": "Concept ACK, see #32377, with two caveats:\n\n[quoted text omitted]\nI think this risks introducing a new effective checkpoint. If the snapshot were malicious, normal users couldn't sync and a subset may decide to restart in AssumeUTXO mode.\n\nIt seems better to emit a warning, severe enough that the GUI shows it, but not aborting. It's a clear sign there's something wrong with the software. Even though it doesn't affect them, they should seek advice, since it's a giant red flag.\n\n(in such a malicious scenario, developers would presumably change the warning to an error, exactly to achieve this wrong behavior)\n\nThis was already discussed in https://github.com/bitcoin/bitcoin/issues/32377#issuecomment-2851372290, but the implementation doesn't match the conclusion there.\n\n[quoted text omitted]\nMaybe limit the scope of this PR by only _checking_ the latest hash, but not (yet) deleting the other hashes.\n\n---\n\nYou mentioned offline that the performance impact of this extra check is minimal. It would be good to provide some numbers in the PR description.\n\nI'm not up to speed on how often and when we flush the chainstate to disk during IBD nowadays. How does this interact with that?",
   "id": "c:6229005853",
   "url": "https://github.com/bitcoin/bitcoin/pull/36543#issuecomment-6229005853",
   "hash": "a861a2be8ebd4021",
   "edited": "2026-10-17T03:40:42Z"
  },
  {
   "t": "2026-10-17T05:16:28Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK\n\nAs there is a hash provided that requires trust, the more checks the better. It makes sense to use all nodes performing IBD to also perform this check, as it's relatively cheap to do.\n\n[quoted text omitted]\nTwo suggestions:\n\n- Introduce an option to skip the new AssumeUTXO hash check (may be a hidden option) -- useful in itself.\n- In the unlikely case a hash mismatch is found, abort, and advice the user to shout out and restart with the ignore AssumeUTXO hash check option. That way the effect is hard to ignore by the user, but easy to bypass.\n\n[quoted text omitted]\nFor clarity it's worth mentioning that the latest hash a user is checking may not be the same as the latest hash of the latest release, nor the one used by most nodes in the network. (For the sake of example, assuming v35 has been released, and a majority of the network is on v34, when a user is running v33, the checked hash will not match the hash used by many other nodes.) Note that it would be desirable to check the hash of newer versions, but of course it's not possible to hardcode hashes of future releases in the source code. So not checking the non-latest hashes is fine, but it's useful to keep them for visibility.",
   "id": "c:6230060049",
   "url": "https://github.com/bitcoin/bitcoin/pull/36543#issuecomment-6230060049",
   "hash": "97e19848b9ad1636",
   "edited": "2026-10-17T05:25:39Z"
  },
  {
   "t": "2026-10-24T04:21:13Z",
   "kind": "comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "text": "What's the performance impact when hitting such a block?\n\nDoesn't this make the result of a release including bad snaphot data worse for everyone -- Alice who does IBD has her node crash despite having good data that she fully verified, and Bob who uses the bad snapshot still potentially fails to an incorrect chain unless Alice has already done her IBD run and Bob happens to hear Alice complain about the failure?\n\nHaving predictable snapshot points (every 14112 blocks [per bip452](https://github.com/bitcoin/bips/pull/2137/files)) would allow every node to calculate the appropriate hash (in the background on a leveldb snapshot without cs_main held, hopefully) and remember it, prior to a new release being considered, making verification much easier, and consistent across implementations. Including the block's muhash in the assumeutxo set would also probably work here, since muhash is already calculated and stored in coinstatsindex.",
   "id": "c:6379792380",
   "url": "https://github.com/bitcoin/bitcoin/pull/36543#issuecomment-6379792380",
   "hash": "b384afb51e37da05",
   "edited": null
  },
  {
   "t": "2026-10-25T11:38:48Z",
   "kind": "review",
   "who": "fametrano",
   "assoc": "NONE",
   "state": "COMMENTED",
   "commit": "97b2138e39ce34e239039e7d38d95e15f3c19a9d",
   "text": "Concept ACK\n\nOne issue should be fixed before merge: a node that fails the new check aborts on an assertion, and a plain restart then syncs past the commitment without checking it again. I built 97b2138e39 with one digit of the regtest commitment's hash at height 299 changed, and let it sync the `feature_assumeutxo.py` chain to 310 from an unmodified node. In nine runs out of nine it logged the mismatch and the fatal error, then died with SIGABRT on `Assertion failed: (hashPrevBlock == view.GetBestBlock())` in `ConnectBlock`. Restarted with no new option, it starts at 299, syncs to 310 and never hashes the UTXO set again. `-reindex-chainstate` stops it once more, and a plain restart then syncs past 299 again. The same change on cce32b1687 syncs to 310. I can share the harness.\n\nIn `ConnectTip`, block 299 reaches the coins cache and the disk before the check, and the failure returns before `m_chain.SetTip`. The coins are at 299 while the chain stays at 298. Connecting 299 again, when the next block arrives during shutdown, trips the assert. The next start takes its tip from the coins. Moving the check after `UpdateTip` is not enough: on regtest the node then aborts in `CheckBlockIndex` instead. The unit test processes no block after the mismatch, so it reaches neither assert.\n\nOn warning versus stopping (Sjors, ajtowns): as it stands, the stop lasts only until the next restart.",
   "id": "r:5643008477",
   "url": "https://github.com/bitcoin/bitcoin/pull/36543#pullrequestreview-5643008477",
   "hash": "f4a1237f485f459e",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-15T13:40:29Z",
   "action": "labeled",
   "label": "Validation",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-15T14:01:06Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-15T14:58:51Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 8909,
 "text_tokens_estimate": 2227,
 "changed_paths": [
  "doc/assumeutxo.md",
  "doc/release-notes-assumeutxo-latest-snapshot.md",
  "doc/release-process.md",
  "src/external_signer.cpp",
  "src/kernel/chainparams.cpp",
  "src/kernel/chainparams.h",
  "src/node/blockstorage.cpp",
  "src/rpc/blockchain.cpp",
  "src/test/fuzz/utxo_snapshot.cpp",
  "src/test/util/setup_common.cpp",
  "src/test/util/setup_common.h",
  "src/test/validation_chainstatemanager_tests.cpp",
  "src/test/validation_tests.cpp",
  "src/util/vector.h",
  "src/validation.cpp",
  "src/validation.h",
  "test/functional/feature_assumeutxo.py"
 ],
 "files": [
  {
   "path": "doc/assumeutxo.md",
   "add": 15,
   "del": 1
  },
  {
   "path": "doc/release-notes-assumeutxo-latest-snapshot.md",
   "add": 11,
   "del": 0
  },
  {
   "path": "doc/release-process.md",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/external_signer.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/kernel/chainparams.cpp",
   "add": 21,
   "del": 116
  },
  {
   "path": "src/kernel/chainparams.h",
   "add": 3,
   "del": 11
  },
  {
   "path": "src/node/blockstorage.cpp",
   "add": 2,
   "del": 3
  },
  {
   "path": "src/rpc/blockchain.cpp",
   "add": 2,
   "del": 5
  },
  {
   "path": "src/test/fuzz/utxo_snapshot.cpp",
   "add": 14,
   "del": 6
  },
  {
   "path": "src/test/util/setup_common.cpp",
   "add": 18,
   "del": 2
  },
  {
   "path": "src/test/util/setup_common.h",
   "add": 7,
   "del": 0
  },
  {
   "path": "src/test/validation_chainstatemanager_tests.cpp",
   "add": 55,
   "del": 9
  },
  {
   "path": "src/test/validation_tests.cpp",
   "add": 5,
   "del": 17
  },
  {
   "path": "src/util/vector.h",
   "add": 0,
   "del": 12
  },
  {
   "path": "src/validation.cpp",
   "add": 48,
   "del": 31
  },
  {
   "path": "src/validation.h",
   "add": 6,
   "del": 0
  },
  {
   "path": "test/functional/feature_assumeutxo.py",
   "add": 7,
   "del": 5
  }
 ],
 "test_lines": 145,
 "git": {
  "head": "97b2138e39ce34e239039e7d38d95e15f3c19a9d",
  "head_matches_backup": true,
  "base": "cce32b16874e85e8a00cfa1a2190f0b6f4dcaf66",
  "patch_id": "4d736a78c7ee8748",
  "commits": [
   {
    "sha": "1606010c94",
    "subject": "test: move AssumeUTXO commitments into test fixtures",
    "files": 7,
    "add": 65,
    "del": 38
   },
   {
    "sha": "59eeb531b0",
    "subject": "chainparams: use the latest AssumeUTXO commitment",
    "files": 11,
    "add": 21,
    "del": 63
   },
   {
    "sha": "9646f56002",
    "subject": "refactor: extract AssumeUTXO hash verification",
    "files": 2,
    "add": 32,
    "del": 17
   },
   {
    "sha": "27705e415f",
    "subject": "refactor: store one AssumeUTXO commitment",
    "files": 2,
    "add": 24,
    "del": 94
   },
   {
    "sha": "36a2a70452",
    "subject": "validation: verify the provided AssumeUTXO hash",
    "files": 3,
    "add": 38,
    "del": 4
   },
   {
    "sha": "c296123d4d",
    "subject": "validation: propagate snapshot verification errors",
    "files": 2,
    "add": 14,
    "del": 7
   },
   {
    "sha": "97b2138e39",
    "subject": "doc: describe latest AssumeUTXO commitment checks",
    "files": 3,
    "add": 27,
    "del": 2
   }
  ],
  "patch_truncated": true
 },
 "patch_id": "4d736a78c7ee8748",
 "input_hash": "ea9a33e47ddba02f",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}