{
 "number": 36066,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36066",
 "title": "validation: Separate check-only version of ConnectBlock",
 "author": "optout21",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-08-24T13:26:02Z",
 "updated_at": "2026-09-16T13:30:38Z",
 "age_days": 24,
 "draft": false,
 "labels": [
  "Validation"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "fe970b7b19ea944fcbc9159345217ee7fbd02238",
 "head_ref": "connectblock-checkonly",
 "head_repo": "optout21/bitcoin",
 "head_history": [
  {
   "t": "2026-09-11T06:40:35Z",
   "sha": "36739b5f0caeaeb2ac7e77eae75ff0954b8fcd0b"
  },
  {
   "t": "2026-09-11T08:00:24Z",
   "sha": "ddc6be9d37699473658c3da55cb6e4590ac6f0b3"
  },
  {
   "t": "2026-09-11T08:22:31Z",
   "sha": "fe970b7b19ea944fcbc9159345217ee7fbd02238"
  }
 ],
 "additions": 65,
 "deletions": 20,
 "changed_files": 4,
 "commit_count": 1,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "l0rinc",
      "url": "https://github.com/bitcoin/bitcoin/pull/36066#pullrequestreview-5014675288"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36000,
     "title": "validation: prefetch blocks while connecting",
     "author": "l0rinc"
    },
    {
     "number": 35751,
     "title": "validation: use parallel input prevout fetching in TestBlockValidity",
     "author": "andrewtoth"
    },
    {
     "number": 35646,
     "title": "RFC: Separate out runtime errors from BlockValidationState using `util::Expected`",
     "author": "yuvicc"
    },
    {
     "number": 35570,
     "title": "refactor: Change some validation.cpp methods to return BlockValidationState",
     "author": "optout21"
    },
    {
     "number": 35511,
     "title": "RFC: consensus: Make `CAmount` a class",
     "author": "hodlinator"
    },
    {
     "number": 29700,
     "title": "kernel, refactor: return error status on all fatal errors",
     "author": "ryanofsky"
    }
   ]
  }
 },
 "acks_parsed": {
  "l0rinc": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-26T19:33:50Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 1,
  "distinct_reviewers": [
   "l0rinc",
   "purpleKarrot"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-11T08:22:31Z",
  "last_reviewer_activity": "2026-08-26T19:33:50Z",
  "last_reviewer": "l0rinc",
  "author_silent_days": 6,
  "waiting_on_author_days": 0,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   35187,
   35850,
   35904
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35187,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "kernel: Block validation without a complete UTXO set"
   },
   {
    "number": 35850,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-08-27",
    "title": "fuzz: Implement `connect_block` harness"
   },
   {
    "number": 35904,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Factoring out a stateless, side-effect free validation library"
   }
  ],
  "conflicts": [
   36000,
   35751,
   35646,
   35570,
   35511,
   29700
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/validation.cpp",
  "src/validation.h"
 ],
 "body": "This is a small code-hygiene refactor to `ConnectBlock` method.\n\nProblem:  In the \"check-only\" mode of `ConnectBlock` the mutability of the `CBlockIndex` parameter is inconsistent with the semantics. In the default operation, `ConnectBlock` changes the provided `CBlockIndex`. However, it has a special \"check-only\" mode, where the provided chain is not changed. This mode is used from only one call site (`TestBlockValidity`), and is signaled by the `fJustCheck = true` flag.\n\nSolution:  Provide two versions of `ConnectBlock`: one with mutable chain and one \"check-only\" with const chain. There is no need for the `fJustCheck` flag. Both reuse the same internal implementation.\n\nBenefits:\n- The mutability of the `pindex` parameter is in sync with the semantics\n- One less feature flag (`fJustCheck`)\n- Calling \"check-only\" mode is smoother, creating a dummy mutable `CBlockIndex` is now unnecessary\n\nAdditional details:\n- This has been triggered by #35187, there is also a new usage of \"check-only\" `ConnectBlock`, with dummy `CBlockIndex`.\n- The \"check-only\" mode was introduced as early as 3cd01fdf0e540c4e06cd27b6c0d6b6abc00767d1 .\n- No behavior change (pure refactor).\n- Some internal variables (`blockundo`,  `nInputs`, `nSigOpsCost`) are now outputs of the internal method, and used in the default outer method (`ConnectBlock`).\n- `SetBestBlock` has been moved to the outer `ConnectBlock`. It was also present in the special genesis block early return, this is preserved.\n- The tracepoints are in the outer methods\n- Timing is present both in the inner method (logs) and the outer methods (logs, tracepoint)",
 "commits": [
  {
   "sha": "fe970b7b19ea944fcbc9159345217ee7fbd02238",
   "date": "2026-09-11T08:17:49Z",
   "message": "validation: Separate out check-only version of ConnectBlock\n\nNew `TestConnectBlock` method for the check-only variant of `ConnectBlock`,\nwith const `CBlockIndex` parameter. The no-longer-needed `fJustCheck` flag\nis dropped."
  }
 ],
 "timeline": [
  {
   "t": "2026-08-25T03:29:39Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "0e832dbd0488bb358b7505de5326e20e068eca9c",
   "in_reply_to": null,
   "text": "`ConnectBlockChecks()` already hashes the block and verifies that it matches `pindex`.\nCould we use the stored index hash here instead?\n\n```suggestion\n        pindex->GetBlockHash().data(),\n```"
  },
  {
   "t": "2026-08-25T04:23:24Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "0e832dbd0488bb358b7505de5326e20e068eca9c",
   "in_reply_to": null,
   "text": "`TestConnectBlock`  is never called with a possibly-nullptr block index, consider a const reference instead:\n\npass block index by reference\n\n```patch\ndiff --git a/src/validation.cpp b/src/validation.cpp\nindex a4b86f18a9..83bd02944c 100644\n--- a/src/validation.cpp\n+++ b/src/validation.cpp\n@@ -2700,7 +2700,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,\n     return true;\n }\n\n-bool Chainstate::TestConnectBlock(const CBlock& block, BlockValidationState& state, const CBlockIndex* pindex,\n+bool Chainstate::TestConnectBlock(const CBlock& block, BlockValidationState& state, const CBlockIndex& index,\n                                    CCoinsViewCache& view)\n {\n     AssertLockHeld(cs_main);\n@@ -2708,7 +2708,7 @@ bool Chainstate::TestConnectBlock(const CBlock& block, BlockValidationState& sta\n     CBlockUndo blockundo;\n     int nInputs = 0;\n     int64_t nSigOpsCost = 0;\n-    return ConnectBlockChecks(block, state, pindex, view, /*fJustCheck=*/true, blockundo, nInputs, nSigOpsCost);\n+    return ConnectBlockChecks(block, state, &index, view, /*fJustCheck=*/true, blockundo, nInputs, nSigOpsCost);\n }\n\n CoinsCacheSizeState Chainstate::GetCoinsCacheSizeState()\n@@ -4578,7 +4578,7 @@ BlockValidationState TestBlockValidity(\n     CCoinsViewCache view_dummy(&chainstate.CoinsTip());\n\n     // Call TestConnectBlock(), it updates, and does not clear, validation caches.\n-    if (!chainstate.TestConnectBlock(block, state, &index_dummy, view_dummy)) {\n+    if (!chainstate.TestConnectBlock(block, state, index_dummy, view_dummy)) {\n         if (state.IsValid()) NONFATAL_UNREACHABLE();\n         return state;\n     }\ndiff --git a/src/validation.h b/src/validation.h\nindex dfc87331e8..1d2c773296 100644\n--- a/src/validation.h\n+++ b/src/validation.h\n@@ -784,8 +784,8 @@ public:\n     //! Connect a block to the chain, updating pindex and the block undo/index files on disk.\n     bool ConnectBlock(const CBlock& block, BlockValidationState& state, CBlockIndex* pindex,\n         CCoinsViewCache& view) EXCLUSIVE_LOCKS_REQUIRED(cs_main);\n-    //! Run the same validity checks as ConnectBlock() without mutating pindex or writing anything to disk.\n-    bool TestConnectBlock(const CBlock& block, BlockValidationState& state, const CBlockIndex* pindex,\n+    //! Run the same validity checks as ConnectBlock() without mutating the block index or writing anything to disk.\n+    bool TestConnectBlock(const CBlock& block, BlockValidationState& state, const CBlockIndex& index,\n         CCoinsViewCache& view) EXCLUSIVE_LOCKS_REQUIRED(cs_main);\n\n     // Apply the effects of a block disconnection on the UTXO set.\n```"
  },
  {
   "t": "2026-08-25T04:25:03Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/validation.h",
   "commit": "36739b5f0caeaeb2ac7e77eae75ff0954b8fcd0b",
   "in_reply_to": null,
   "text": "Some of the related comments could be updated now.\n\nupdate check-only block comments\n\n```patch\ndiff --git a/src/validation.cpp b/src/validation.cpp\nindex 611fc16805..3c0dd35729 100644\n--- a/src/validation.cpp\n+++ b/src/validation.cpp\n@@ -4568,7 +4568,7 @@ BlockValidationState TestBlockValidity(\n         return state;\n     }\n\n-    // We don't want ConnectBlock to update the actual chainstate, so create\n+    // We don't want TestConnectBlock to update the actual chainstate, so create\n     // a cache on top of it, along with a dummy block index.\n     CBlockIndex index_dummy{block};\n     uint256 block_hash(block.GetHash());\ndiff --git a/test/functional/interface_usdt_utxocache.py b/test/functional/interface_usdt_utxocache.py\nindex da90790d70..37cc205541 100755\n--- a/test/functional/interface_usdt_utxocache.py\n+++ b/test/functional/interface_usdt_utxocache.py\n@@ -241,7 +241,7 @@ class UTXOCacheTracepointTest(BitcoinTestFramework):\n         # of the UTXO set (see CoinsTip() of CCoinsViewCache). However, in some cases\n         # temporary clones of the active cache are made. For example, during mining with\n         # the generate RPC call, the block is first tested in TestBlockValidity(). There,\n-        # a clone of the active cache is modified during a test ConnectBlock() call.\n+        # a clone of the active cache is modified during a TestConnectBlock() call.\n         # These are implementation details we don't want to test here. Thus, after\n         # mining, we invalidate the block, start the tracing, and then trace the cache\n         # changes to the active utxo cache.\n```"
  },
  {
   "t": "2026-08-25T09:49:56Z",
   "kind": "comment",
   "who": "purpleKarrot",
   "assoc": "MEMBER",
   "text": "What about the mutability and mutations of the `Chainstate` (the implicit `this` argument)?\nWith the proposed change, calling `TestConnectBlock` still has the side effect of mutating `num_blocks_total`, for example.\n\nPlease have a look at https://github.com/bitcoin/bitcoin/issues/35904, which proposes extracting the block validation parts from `ConnectBlock` into a function that has no side effects."
  },
  {
   "t": "2026-08-25T10:07:42Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "Thanks for the feedback, @purpleKarrot ! I will check out #35904 (I've seen 35906, but not 35904).\nI'm also continuing exploring, currently looking at these small issues:\n- in check-only mode, block undo data is computed unnecessarily (a small optimization potential).\n- in check-only mode, the coins view that is being updated can be created inside (by `TestConnectBlock`), so the caller does not need to create a dummy. Also, the CBlockIndex supplied can be simplified (not to require a CBlockIndex that already contains the new block)."
  },
  {
   "t": "2026-08-26T19:33:50Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "0e832dbd0488bb358b7505de5326e20e068eca9c",
   "text": "Concept ACK\n\nSeparating the check-only entry point makes the `CBlockIndex` mutability clearer and removes `fJustCheck` from the public `ConnectBlock()` interface.\nI've split this locally (consider splitting the change similarly, see https://github.com/bitcoin/bitcoin/compare/master...l0rinc:bitcoin:l0rinc/validation-test-connect-block):\n* add and route through `TestConnectBlock()`\n* expose the connection outputs\n* handle failed checks and genesis in `ConnectBlock()`\n* move the ordered undo/index/tracepoint updates\n* then make the check-only index const.\n\nMy pending comments cover reusing the stored index hash, passing the index by const reference, and updating the related comments. The broader `Chainstate` side effects are already being discussed above.\n\n[quoted text omitted]\n+1 for these functional-style refactors (though, as mentioned, exceptions are the opposite of that), but given how consensus-critical this code is, I\u2019d prefer moving toward side-effect-free validation through tiny, trivial-to-review changes, with this PR limited to making the `CBlockIndex` mutability explicit (and split into smaller changes that are all obviously correct)."
  },
  {
   "t": "2026-09-04T10:42:30Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "@purpleKarrot, to revisit your comment:\nI agree that the right approach would be to rework `ConnectBlock` to take whatever data it needs, and produce update data as output, without side effects. The update data then can be applied (in normal connect block use case), or discarded (check only use case). This structure is also more compatible with validation needs where certain parts (e.g. the UTXO set) is supplied differently (see #35187). I see that this change is not trivial (and touching critical code).\n\nI think that the small change from this PR is compatible with the approach laid out in #35904. It is a small step, but it makes the input and update data slightly more explicit.\n\nI've also made an attempt to make `ConnectBlock` static, thus exposing the dependencies even better (https://github.com/optout21/bitcoin/pull/23), but I'm not sure how to progress with that.\n\nFor reference, I detail here the updates that happen in `ConnectBlock`:\n\n- The passed `CCoinsView` is updated with the coin changes (spent&new)\n- The script execution cache is updated (`ValidationCache` `ChainstateManager.m_validation_cache`)\n- `ChainstateManager`'s `num_blocks_total` is incremented\n- `ChainstateManager`'s `time_Xxx` fields are incremented (`time_check`, `time_forks`, etc.)\n- `Chainstate::m_last_script_check_reason_logged` may be updated\n- `BlockManager` is updated with the block undo data (skipped for JustCheck)\n- `BlockManager::m_dirty_blockindex` is updated (in case of script failure; skipped for JustCheck)"
  },
  {
   "t": "2026-09-11T06:17:31Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "0e832dbd0488bb358b7505de5326e20e068eca9c",
   "in_reply_to": 3849332970,
   "text": "Good point, that's more optimal. I will store the block hash in a local variable, right after `ConnectBlockChecks` call, and use it from there. This way it's more similar how the `ConnectBlockChecks` does it internally."
  },
  {
   "t": "2026-09-11T06:33:35Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "0e832dbd0488bb358b7505de5326e20e068eca9c",
   "in_reply_to": 3849546706,
   "text": "Good point! Applied. (I've overlooked this, even though I have a separate open PR with exactly such changes...)"
  },
  {
   "t": "2026-09-11T06:33:49Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.h",
   "commit": "36739b5f0caeaeb2ac7e77eae75ff0954b8fcd0b",
   "in_reply_to": 3849553446,
   "text": "Taken."
  },
  {
   "t": "2026-09-11T06:40:35Z",
   "kind": "force_push",
   "who": "optout21",
   "commit": "36739b5f0caeaeb2ac7e77eae75ff0954b8fcd0b"
  },
  {
   "t": "2026-09-11T06:40:35Z",
   "kind": "review",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "0e832dbd0488bb358b7505de5326e20e068eca9c",
   "text": "Applied review suggestions, minor changes. Thanks for the review, and sorry for the delay.\n`git diff 0e832dbd0488bb358b7505de5326e20e068eca9c..36739b5f0caeaeb2ac7e77eae75ff0954b8fcd0b`"
  },
  {
   "t": "2026-09-11T07:58:42Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "UPDATE: Was due to silent merge conflict, fixed.\n\nI'm struggling with the CI build errors. Fuzz compile fails for `src/test/fuzz/connect_block.cpp`, however, I can't find that file, by name, or by contents. Locally fuzz compile works with the same commit (36739b5f0caeaeb2ac7e77eae75ff0954b8fcd0b).\n\n```\n/home/runner/work/_temp/src/test/fuzz/connect_block.cpp:476:68: error: too many arguments to function call, expected 4, have 5\n  472 |     bool connected = active_chainstate.ConnectBlock(block,\n```"
  },
  {
   "t": "2026-09-11T08:00:24Z",
   "kind": "force_push",
   "who": "optout21",
   "commit": "ddc6be9d37699473658c3da55cb6e4590ac6f0b3"
  },
  {
   "t": "2026-09-11T08:03:27Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "I've noticed an accidental mistake in the newly-touched `test/functional/interface_usdt_utxocache.py` (the first '#' character was dropped), fixed.\nI doubt this has anything to do with the CI failures though.\n\n`git diff 36739b5f0caeaeb2ac7e77eae75ff0954b8fcd0b..ddc6be9d37699473658c3da55cb6e4590ac6f0b3`"
  },
  {
   "t": "2026-09-11T08:09:34Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "CI failure is due to silent merge conflict with #35850, fixing."
  },
  {
   "t": "2026-09-11T08:22:29Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "Rebased and fixed silent merge error, fuzz compile error due to newly introduced `connect_block.cpp` fuzzer (in #35850, @marcofleon).\n\n`git range-diff ddc6be9d37699473658c3da55cb6e4590ac6f0b3...fe970b7b19ea944fcbc9159345217ee7fbd02238`"
  },
  {
   "t": "2026-09-11T08:22:31Z",
   "kind": "force_push",
   "who": "optout21",
   "commit": "fe970b7b19ea944fcbc9159345217ee7fbd02238"
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-24T13:26:06Z",
   "action": "labeled",
   "label": "Validation",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-11T07:34:28Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-11T09:26:17Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-08-24T15:51:57Z",
   "kind": "ready_for_review",
   "who": "optout21"
  },
  {
   "t": "2026-09-11T07:47:04Z",
   "kind": "closed",
   "who": "optout21"
  },
  {
   "t": "2026-09-11T07:47:08Z",
   "kind": "reopened",
   "who": "optout21"
  }
 ],
 "text_chars": 11582,
 "text_tokens_estimate": 2895,
 "changed_paths": [
  "src/test/fuzz/connect_block.cpp",
  "src/validation.cpp",
  "src/validation.h",
  "test/functional/interface_usdt_utxocache.py"
 ],
 "files": [
  {
   "path": "src/test/fuzz/connect_block.cpp",
   "add": 4,
   "del": 5
  },
  {
   "path": "src/validation.cpp",
   "add": 44,
   "del": 13
  },
  {
   "path": "src/validation.h",
   "add": 16,
   "del": 1
  },
  {
   "path": "test/functional/interface_usdt_utxocache.py",
   "add": 1,
   "del": 1
  }
 ],
 "test_lines": 11,
 "git": {
  "head": "fe970b7b19ea944fcbc9159345217ee7fbd02238",
  "head_matches_backup": true,
  "base": "a6b330973e640987713b14499830fe62bc9f1807",
  "commits": [
   {
    "sha": "fe970b7b19",
    "subject": "validation: Separate out check-only version of ConnectBlock",
    "files": 4,
    "add": 65,
    "del": 20
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "41c79971d9b5a60e",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}