{
 "number": 36149,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36149",
 "title": "validation: Leave pruned blocks in `m_blocks_unlinked`, fix another `nSequenceId` issue",
 "author": "mzumsande",
 "author_association": "MEMBER",
 "created_at": "2026-09-02T14:33:49Z",
 "updated_at": "2026-09-09T21:50:40Z",
 "age_days": 15,
 "draft": true,
 "labels": [
  "Validation"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "40a31d97ab118cb030af7d560aedbade3ae4cd36",
 "head_ref": "202609_fix_cbi_pruning_unlinked",
 "head_repo": "mzumsande/bitcoin",
 "head_history": [
  {
   "t": "2026-09-02T14:35:16Z",
   "sha": "40a31d97ab118cb030af7d560aedbade3ae4cd36"
  }
 ],
 "additions": 69,
 "deletions": 37,
 "changed_files": 5,
 "commit_count": 4,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "marcofleon",
      "url": "https://github.com/bitcoin/bitcoin/pull/36149#pullrequestreview-5153355230"
     }
    ],
    "concept_ack": [
     {
      "login": "l0rinc",
      "url": "https://github.com/bitcoin/bitcoin/pull/36149#issuecomment-5513652613"
     },
     {
      "login": "jeanpablojp",
      "url": "https://github.com/bitcoin/bitcoin/pull/36149#pullrequestreview-5136147146"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "l0rinc": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-02T17:29:54Z",
   "stale": false
  },
  "jeanpablojp": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-08T00:32:58Z",
   "stale": false
  },
  "marcofleon": {
   "kind": "ack",
   "hash": "40a31d97ab118cb030af7d560aedbade3ae4cd36",
   "t": "2026-09-09T11:24:29Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "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": [
   "jeanpablojp",
   "l0rinc",
   "marcofleon"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-09T21:50:29Z",
  "last_reviewer_activity": "2026-09-09T11:24:29Z",
  "last_reviewer": "marcofleon",
  "author_silent_days": 7,
  "waiting_on_author_days": 0,
  "days_since_update": 7
 },
 "refs": {
  "mentioned": [
   31512,
   34521,
   35070,
   36021
  ],
  "depends_on": [],
  "fixes": [
   31512,
   36021
  ],
  "linked_issues": [
   {
    "number": 31512,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "validation: CheckBlockIndex can fail in combination with pruning"
   },
   {
    "number": 36021,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "validation: `Assertion failed: ((pindexFirstNeverProcessed == nullptr || pindex == snap_base) == pindex->HaveNumChainTxs())`"
   }
  ],
  "references": [
   {
    "number": 31512,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "validation: CheckBlockIndex can fail in combination with pruning"
   },
   {
    "number": 36021,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "validation: `Assertion failed: ((pindexFirstNeverProcessed == nullptr || pindex == snap_base) == pindex->HaveNumChainTxs())`"
   },
   {
    "number": 34521,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-03-06",
    "title": "validation: fix UB in `LoadChainTip`"
   },
   {
    "number": 35070,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-06-23",
    "title": "validation: prevent FindMostWorkChain from causing UB"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/node/blockstorage.cpp",
  "src/node/blockstorage.h",
  "test/functional/feature_prune_stale_fork.py"
 ],
 "body": "`m_blocks_unlinked` serves two purposes:\n 1. finding blocks to `setBlockIndexCandidates` once their parents are\n       received\n 2. setting `m_chain_tx_count` once the parents are received\n\nIn case the parent block of a previously pruned block is received,  2. is currently not being done, because pruning removed the entries from `m_blocks_unlinked`.\nThis can result in `CheckBlockIndex` failures (#31512, #36021) and incorrect rpc results, plus we are in a temporarily inconsistent state (`m_chain_tx_count` is currently not set, but would get set if we restarted).\n\nThis scenario is unlikely to happen during normal node operation unless there are huge reorgs, but it can happen, for example, in `getblockfrompeer` scenarios.\nFix this by leaving pruned blocks in `m_chain_tx_count`, and also adding them on restart.\n\nThis was one of two reasons, the fuzz target `block_index_tree` couldn't cover pruning of blocks not in the main chain.\n\nThe other one is similar to #34521 and #35070 - if we receive a block a second time (after pruning), we could change `nSequenceId` while the block is in `setBlockIndexCandidates` - fix this by attempting to remove it from the set before re-adding it.\n\nAfter the two issues are fixed, the restriction from `block_index_tree` fuzz target is removed.\n\nFixes #36021\nFixes #31512",
 "commits": [
  {
   "sha": "7c7ccc9cc0939e00f060b49ec476a761f2c1c85e",
   "date": "2026-09-02T14:34:22Z",
   "message": "validation: keep pruned blocks in m_blocks_unlinked\n\nm_blocks_unlinked serves two purposes:\n1. finding blocks to SetBlockIndexCandidates once their parents are\n   received\n2. Setting m_chain_tx_count once the parents are received\n\nIn case that a parent of a pruned block is received,\n2. is currently not being done, because pruning removed the entries.\nThis can result in CheckBlockIndex failures, and an inconsistent state\n(because in case of a restart, m_chain_tx would be set).\nThis is unlikely to happen during normal node operation unless\nthere are huge reorgs, but it can happen, for example, in getblockfrompeer scenarios.\n\nFix this by keeping pruned blocks in m_blocks_unlinked and also removing\nthe BLOCK_HAVE_DATA requirement during startup."
  },
  {
   "sha": "5d5af40fcb55b0a2452d426c9b49c4eb55eb28f5",
   "date": "2026-09-02T14:34:29Z",
   "message": "test: add functional test for parents of pruned blocks\n\nThis test would lead to a CheckBlockIndex assertion fail without\nthe changes of the previous commit."
  },
  {
   "sha": "80477ba97820485c8371f71d0b46b7bb23325cf2",
   "date": "2026-09-02T14:34:30Z",
   "message": "validation: don't change nSequenceId of a block that is a chain candidate\n\nnSequenceId is one of the sort keys of setBlockIndexCandidates, so changing it\nfor a block that currently is in one of these sets corrupts their ordering,\nafter which lookups for other entries can fail.\n\nThis is also necessary for the block_index_tree fuzz test extension in the\nfollowing commit, which would otherwise trip on this."
  },
  {
   "sha": "40a31d97ab118cb030af7d560aedbade3ae4cd36",
   "date": "2026-09-02T14:34:30Z",
   "message": "fuzz: allow to prune out-of-chain blocks in block_index_tree\n\nAfter the issues with m_blocks_unlinked and\nsetBlockIndexCandidates have been resolved in previous commits,\nthe previous restriction to the fuzz test does no longer apply,\nand we can also prune out-of-chain blocks."
  }
 ],
 "timeline": [
  {
   "t": "2026-09-02T14:35:16Z",
   "kind": "force_push",
   "who": "mzumsande",
   "commit": "40a31d97ab118cb030af7d560aedbade3ae4cd36"
  },
  {
   "t": "2026-09-02T17:29:54Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Concept ACK, thanks for fixing it. I will try to review it a bit later."
  },
  {
   "t": "2026-09-08T00:32:58Z",
   "kind": "review",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "40a31d97ab118cb030af7d560aedbade3ae4cd36",
   "text": "Concept ACK\n\nTaking out the `setBlockIndexCandidates` erase, the extended `block_index_tree` target breaks on an input that is already in the qa-assets corpus."
  },
  {
   "t": "2026-09-08T00:32:58Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/node/blockstorage.h",
   "commit": "40a31d97ab118cb030af7d560aedbade3ae4cd36",
   "in_reply_to": null,
   "text": "nit (feel free to ignore): `predecessors` appears only here in `src/`, and `ancestors` sounds to be the preference, including the comment this line replaces.\n\n```suggestion\n     * but B is not a candidate for connection, because for A or one of its ancestors\n```"
  },
  {
   "t": "2026-09-08T00:32:58Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "test/functional/feature_prune_stale_fork.py",
   "commit": "40a31d97ab118cb030af7d560aedbade3ae4cd36",
   "in_reply_to": null,
   "text": "Pre-existing, so feel free to ignore.\nThis assertion is what made me look.\n\nThe `getchaintips` help calls `valid-headers` \"All blocks are available for this branch\", but the only availability test behind the status is `HaveNumChainTxs()`, whose own doc says it \"Does not imply the transactions are still stored on disk\". I get the same on master with a stale fork received in full and then pruned, where `getblock` on those blocks fails with \"Block not available (pruned data)\".\n\nWould \"received at some point\" be closer than \"are available\" there?"
  },
  {
   "t": "2026-09-09T11:01:04Z",
   "kind": "review_comment",
   "who": "marcofleon",
   "assoc": "MEMBER",
   "path": "test/functional/feature_prune_stale_fork.py",
   "commit": "5d5af40fcb55b0a2452d426c9b49c4eb55eb28f5",
   "in_reply_to": null,
   "text": "We could also test submitting a missing parent of a pruned child before restarting, as in https://github.com/bitcoin/bitcoin/issues/36021. This test currently passes with the old behavior of [removing](https://github.com/bitcoin/bitcoin/blob/fb26bc9cfefeacda5df08c36a3b2e9f1086e3e48/src/node/blockstorage.cpp#L288-L295) a pruned block from `m_blocks_unlinked`, because it gets added back on start up. Could add a separate 2-block fork to cover the no restart case?"
  },
  {
   "t": "2026-09-09T11:24:29Z",
   "kind": "review",
   "who": "marcofleon",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "40a31d97ab118cb030af7d560aedbade3ae4cd36",
   "text": "ACK 40a31d97ab118cb030af7d560aedbade3ae4cd36\n\nThe main fix in 7c7ccc9cc0939e00f060b49ec476a761f2c1c85e looks correct to me. Ran the fuzz test for a while, no issues. Commit 80477ba97820485c8371f71d0b46b7bb23325cf2 fixes previously existing (potential) UB in `setBlockIndexCandidates` that is now caught by the fuzz target. Nice.\n\nIn the PR description,\n[quoted text omitted]\n\nshould be `m_blocks_unlinked` instead.\n\nAlso, left a non-blocking comment on the functional test.\n\nThanks for fixing this cleanly!"
  },
  {
   "t": "2026-09-09T21:49:07Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "src/node/blockstorage.cpp",
   "commit": "7c7ccc9cc0939e00f060b49ec476a761f2c1c85e",
   "in_reply_to": null,
   "text": "A slightly different approach here (suggested by @l0rinc) would be to remove conditionally  on `m_chain_tx` being set.\nI will explore this next week."
  },
  {
   "t": "2026-09-09T21:50:29Z",
   "kind": "review",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "40a31d97ab118cb030af7d560aedbade3ae4cd36",
   "text": "Thanks for the reviews! I will address the comments (and also most likely rework the PR a little bit, see other comment) next week, putting into draft until then."
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-02T14:33:53Z",
   "action": "labeled",
   "label": "Validation",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-02T14:40:29Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-02T15:51:39Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-09-02T17:40:17Z",
   "kind": "ready_for_review",
   "who": "mzumsande"
  },
  {
   "t": "2026-09-09T21:50:40Z",
   "kind": "convert_to_draft",
   "who": "mzumsande"
  }
 ],
 "text_chars": 5326,
 "text_tokens_estimate": 1331,
 "changed_paths": [
  "src/node/blockstorage.cpp",
  "src/node/blockstorage.h",
  "src/test/fuzz/block_index_tree.cpp",
  "src/validation.cpp",
  "test/functional/feature_prune_stale_fork.py"
 ],
 "files": [
  {
   "path": "src/node/blockstorage.cpp",
   "add": 6,
   "del": 16
  },
  {
   "path": "src/node/blockstorage.h",
   "add": 10,
   "del": 1
  },
  {
   "path": "src/test/fuzz/block_index_tree.cpp",
   "add": 2,
   "del": 14
  },
  {
   "path": "src/validation.cpp",
   "add": 14,
   "del": 3
  },
  {
   "path": "test/functional/feature_prune_stale_fork.py",
   "add": 37,
   "del": 3
  }
 ],
 "test_lines": 56,
 "git": {
  "head": "40a31d97ab118cb030af7d560aedbade3ae4cd36",
  "head_matches_backup": true,
  "base": "128e5c6805a02712ee981b2ca4613dc7d57761ce",
  "commits": [
   {
    "sha": "7c7ccc9cc0",
    "subject": "validation: keep pruned blocks in m_blocks_unlinked",
    "files": 4,
    "add": 23,
    "del": 28
   },
   {
    "sha": "5d5af40fcb",
    "subject": "test: add functional test for parents of pruned blocks",
    "files": 1,
    "add": 37,
    "del": 3
   },
   {
    "sha": "80477ba978",
    "subject": "validation: don't change nSequenceId of a block that is a chain candidate",
    "files": 1,
    "add": 7,
    "del": 0
   },
   {
    "sha": "40a31d97ab",
    "subject": "fuzz: allow to prune out-of-chain blocks in block_index_tree",
    "files": 1,
    "add": 2,
    "del": 6
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "ee43223efb6c3c73",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}