{
 "number": 35000,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35000",
 "title": "test: Add block validation unit tests",
 "author": "ismaelsadeeq",
 "author_association": "MEMBER",
 "created_at": "2026-04-04T01:42:25Z",
 "updated_at": "2026-06-05T16:57:24Z",
 "age_days": 166,
 "draft": false,
 "labels": [
  "Tests"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "ccfc34482d1120aa05d65c83d3e41f8dbee851b3",
 "head_ref": "04-2026-test-block-validity",
 "head_repo": "ismaelsadeeq/bitcoin",
 "head_history": [
  {
   "t": "2026-04-07T19:18:43Z",
   "sha": "f1d2302ecc9abe72f15197f5ce40ed0ed46686a9"
  },
  {
   "t": "2026-04-10T16:11:22Z",
   "sha": "26b1e0b554dcb9b6b28ef33e69eefc9c4e34fbdb"
  },
  {
   "t": "2026-04-23T12:22:39Z",
   "sha": "5ae4c0cc393405176c3a3454bf5ae6ad4fb4fa23"
  },
  {
   "t": "2026-04-28T18:25:03Z",
   "sha": "8e556cdf59db04bb604554e7fb9b8a2ce345f2a3"
  },
  {
   "t": "2026-05-27T14:15:17Z",
   "sha": "0d73f0d9648b0b83ff4966309c510942df050599"
  },
  {
   "t": "2026-05-28T14:43:58Z",
   "sha": "c9d6eb5d9d1db6d6c0ffc9096932d40b121b2875"
  },
  {
   "t": "2026-06-05T16:53:50Z",
   "sha": "ccfc34482d1120aa05d65c83d3e41f8dbee851b3"
  }
 ],
 "additions": 1384,
 "deletions": 0,
 "changed_files": 8,
 "commit_count": 3,
 "size_bucket": "XL",
 "mergeable_state": "blocked",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "approach_ack": [
     {
      "login": "sedited",
      "url": "https://github.com/bitcoin/bitcoin/pull/35000#issuecomment-4295432240"
     }
    ],
    "stale_ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/35000#pullrequestreview-4423709564"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "w0xlt": {
   "kind": "ack",
   "hash": "c9d6eb5d9d1db6d6c0ffc9096932d40b121b2875",
   "t": "2026-06-04T01:09:57Z",
   "stale": true
  },
  "sedited": {
   "kind": "approach_ack",
   "hash": null,
   "t": "2026-04-22T10:26:12Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 1,
  "concept_ack": 0,
  "approach_ack": 1,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Bortlesboat",
   "marcofleon",
   "sedited",
   "stratospher",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "blocked",
  "last_author_activity": "2026-06-05T16:57:24Z",
  "last_reviewer_activity": "2026-06-04T01:09:57Z",
  "last_reviewer": "w0xlt",
  "author_silent_days": 103,
  "waiting_on_author_days": 0,
  "days_since_update": 103
 },
 "refs": {
  "mentioned": [
   32317,
   34651
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 32317,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "kernel: Separate UTXO set access from validation functions"
   },
   {
    "number": 34651,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "fuzz: Block connection and chain reorganization fuzzing harnesses"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/test/fuzz/validation_block_validity.cpp",
  "src/test/validation_block_contextual_tests.cpp",
  "src/test/validation_block_header_tests.cpp",
  "src/test/validation_block_stateful_tests.cpp"
 ],
 "body": "#### Motivation\n\nRecent work on exposing the Bitcoin kernel C header API, [the usage of API](https://delvingbitcoin.org/t/new-utreexo-releases/2371), has increased demand for certain changes to block validation, e,g  an ongoing effort to modularise block validation #32317. However, validation, especially block, is very critical and changes to it demand the utmost scrutiny and a high bar. Which is most of the time slow and careful code review and testing.\nAs noticeable during review of #32317, a subtle issue ([https://github.com/bitcoin/bitcoin/pull/32317#discussion_r2819423976](https://github.com/bitcoin/bitcoin/pull/32317#discussion_r2819423976)) went unnoticed for some time, which suggests that the current test coverage around block validation paths is not sufficiently comprehensive.\nWe have a comprehensive block validation functional test but that is not enough because\nBlock validation today is exercised through multiple entry points, such as `TestBlockValidity`, `ProcessNewBlock`, `ConnectTip`, `VerifyDB`, so having a structured test suite that targets malformed blocks across those validation paths/simulated ones, and testing invariants to ensure subtle regressions do not slip through review is important.\n\nThis PR aims to improve confidence in validation changes by introducing a structured set of unit tests that target specific consensus failure modes, The goal is not to fully de-risk validation changes, but to significantly improve the likelihood of catching subtle bugs early in CI.\n\n#### Changes\n\n- This pr introduces block validation unit tests that are split into header, stateless, contextual, and stateful validation cases.\n\n- The header tests exercise failures related purely to block headers by constructing otherwise valid blocks and mutating header fields, ensuring that header-level checks fail with the expected validation results and reject reasons.\n\n- The stateless tests include malformed block structures such as missing or multiple coinbase transactions, invalid merkle roots, and duplicate transactions. They also cover transaction-level structural issues such as empty inputs or outputs, invalid output values including negative or overflowing amounts, and duplicate inputs within a transaction. Each test constructs a minimally valid block and then applies targeted mutations to trigger specific consensus failures, verifying both the validation result and the exact reject reason string.\n\n- The contextual tests cover rules that depend on chain context. These include enforcing correct coinbase height encoding, detecting coinbase overpayment, preventing premature spending of coinbase outputs, and checking transaction finality conditions. These tests rely on the active chain state and ensure that contextual validation correctly rejects blocks that violate consensus rules tied to block height or historical state.\n\n- The stateful tests construct blocks that spend from UTXO sets and then introduce failures such as missing or already spent inputs, input value overflows, and cases where total inputs are less than total outputs. Script validation failures are also covered by creating real spends that fail script execution under consensus rules. These tests verify that stateful validation correctly enforces value conservation and script correctness.\n\n- To support these tests, reusable helpers are introduced in test/util/block_validity.h. These helpers centralize the invocation of TestBlockValidity and standardize assertions on validation results, reject reasons, and debug messages. They also provide utilities for constructing spendable UTXOs, which simplifies the creation of stateful test scenarios and improves readability and consistency across tests. The aim is to also add other block validation paths in this util that can be easily invoked by this mutated blocks.\n\n- These mutated blocks are passed to `TestBlockValidity`, `ProcessNewBlock` and `ConnectBlock`\n\n#### Open questions\n\nThe unit tests added here cover a substantial portion of consensus rules and some soft fork logic, they are not exhaustive. What is considered sufficient?.",
 "commits": [
  {
   "sha": "c79663e45ec337101544f7bf3914f98b6314d5b9",
   "date": "2026-06-05T16:49:20Z",
   "message": "test: Add block validity test setup and TestBlockValidity tests"
  },
  {
   "sha": "8527fda363ede9a908c83cb0e43a4b52d0b813ba",
   "date": "2026-06-05T16:49:23Z",
   "message": "test: Add ProcessNewBlock block validation path unit tests"
  },
  {
   "sha": "ccfc34482d1120aa05d65c83d3e41f8dbee851b3",
   "date": "2026-06-05T16:51:56Z",
   "message": "test: Add ConnectBlock block validation path unit tests"
  }
 ],
 "timeline": [
  {
   "t": "2026-04-04T06:55:58Z",
   "kind": "comment",
   "who": "stratospher",
   "assoc": "CONTRIBUTOR",
   "text": "have you seen https://github.com/bitcoin/bitcoin/pull/34651 btw? from a quick skim, this fuzz test looks similar. it uses a wrapper around `ConnectBlock` instead of calling it directly. or am I missing something?"
  },
  {
   "t": "2026-04-07T13:57:45Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThanks for pointing that out. I wasn\u2019t aware of it. #34651 and the second commit in this PR, e9115735a45289a783b561fe482ab1540b03d076 have a lot in common: both generate a valid block and then mutate its contents.\n\nHowever, there are some differences. My focus here is on e2e block validity testing, whereas that PR only tests `ConnectBlock`, which skips `ContextualCheckBlock` and `ContextualCheckBlockHeader`.\n\nIf we expose something via the kernel (e.g., block validation with external coins), we already have a framework in place for A/B testing. i.e, if `TestBlockValidityWithSpentTxOuts` is given the same coins and uses a valid known chain index, the output should correlate; otherwise, they may diverge, that kind of thing.\n\nBut it would also be useful to have shared utilities for some of the things we have in common, so that we don't repeat ourselves."
  },
  {
   "t": "2026-04-07T17:34:03Z",
   "kind": "review",
   "who": "Bortlesboat",
   "assoc": "NONE",
   "state": "COMMENTED",
   "commit": "e9115735a45289a783b561fe482ab1540b03d076",
   "text": "Built and ran all four new test suites locally (header, stateless, contextual, stateful), 49/49 pass. Two things inline."
  },
  {
   "t": "2026-04-07T17:34:03Z",
   "kind": "review_comment",
   "who": "Bortlesboat",
   "assoc": "NONE",
   "path": "src/test/validation_block_stateful_tests.cpp",
   "commit": "e9115735a45289a783b561fe482ab1540b03d076",
   "in_reply_to": null,
   "text": "This uses `&&` so it only bails if both `AddCoin` calls fail. If just one fails you get a `bad_optional_access` from `.value()`. The rest of the file uses `if (!outpoint) return;` for each one individually. Should this be `||`?"
  },
  {
   "t": "2026-04-07T17:34:03Z",
   "kind": "review_comment",
   "who": "Bortlesboat",
   "assoc": "NONE",
   "path": "src/test/fuzz/validation_block_validity.cpp",
   "commit": "e9115735a45289a783b561fe482ab1540b03d076",
   "in_reply_to": null,
   "text": "nit: `\"present\"` left over from the old copyright format. The other four files have the right header."
  },
  {
   "t": "2026-04-07T17:50:18Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/test/validation_block_stateful_tests.cpp",
   "commit": "e9115735a45289a783b561fe482ab1540b03d076",
   "in_reply_to": 3046803139,
   "text": "Indeed, I was using `if (outpoint1.has_value() && outpoint2.has_value()) {`\n\nAnd then, executing the test code inside that scope, I changed my mind and just bailed out, but didn't update the `&&` to `|`.\n\nGood catch, I will update."
  },
  {
   "t": "2026-04-07T19:18:43Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "f1d2302ecc9abe72f15197f5ce40ed0ed46686a9"
  },
  {
   "t": "2026-04-07T19:20:59Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "Forced pushed from e9115735a45289a783b561fe482ab1540b03d076 to f1d2302ecc9abe72f15197f5ce40ed0ed46686a9 [compare](https://github.com/bitcoin/bitcoin/compare/e9115735a45289a783b561fe482ab1540b03d076..f1d2302ecc9abe72f15197f5ce40ed0ed46686a9)\n\n- Fixed https://github.com/bitcoin/bitcoin/pull/35000#discussion_r3046803139\n- Fixed https://github.com/bitcoin/bitcoin/pull/35000#discussion_r3046803143\n- use emplace back instead of pushbacks"
  },
  {
   "t": "2026-04-10T14:24:37Z",
   "kind": "comment",
   "who": "marcofleon",
   "assoc": "CONTRIBUTOR",
   "text": "I agree that the fuzz test looks a bit redundant with https://github.com/bitcoin/bitcoin/pull/34651. And I'm not sure it's worth having this PR's harness only as a way to cover the contextual checks.\n\nHere is some validation coverage to compare:\n\n[validation_block_validity](https://marcofleon.github.io/coverage/validation_block_validity/coverage/root/bitcoin/src/validation.cpp.html) from this PR\n\n[connect_block](https://marcofleon.github.io/coverage/connect_block/coverage/root/bitcoin/src/validation.cpp.html) from 34651\n\n[full coverage](https://marcofleon.github.io/coverage/coverage-2026-03-23/coverage/root/bitcoin/src/validation.cpp.html) from all fuzz tests on master\n\nLooking at the coverage, `connect_block` reaches a lot of the same lines as `validation_block_validity` does wrt to the `ConnectBlock` path and it also provides more diverse inputs. And then the contextual checks seem already pretty well covered by our existing fuzz tests (assuming we have strong oracles scattered throughout validation). I think it could make sense to drop the fuzz target here and focus on the unit tests, which look well-written to me."
  },
  {
   "t": "2026-04-10T15:28:57Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "Thanks for taking a look at this @marcofleon\nTo reiterate, when I pushed this, I did know about #34651. My rationale is explained in the description and this comment https://github.com/bitcoin/bitcoin/pull/35000#issuecomment-4199511438\n\nBut I agree, it will be better to spend more effort on #34651 since it has received a couple of review rounds already. I will review that PR and make some suggestions that will make https://github.com/bitcoin/bitcoin/pull/35000#issuecomment-4199511438 achievable there.\n\nIn the meantime, I will remove the fuzz commit here, even if we move forward in this direction, you can argue that the fuzz test deserve it's own PR."
  },
  {
   "t": "2026-04-10T16:11:22Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "26b1e0b554dcb9b6b28ef33e69eefc9c4e34fbdb"
  },
  {
   "t": "2026-04-10T16:14:24Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "Forced pushed from https://github.com/bitcoin/bitcoin/commit/f1d2302ecc9abe72f15197f5ce40ed0ed46686a9  to  https://github.com/bitcoin/bitcoin/commit/26b1e0b554dcb9b6b28ef33e69eefc9c4e34fbdb\n\nPruned the fuzz harness added here in favour of the one in #34651"
  },
  {
   "t": "2026-04-12T18:57:47Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK"
  },
  {
   "t": "2026-04-22T10:01:47Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/validation_block_header_tests.cpp",
   "commit": "26b1e0b554dcb9b6b28ef33e69eefc9c4e34fbdb",
   "in_reply_to": null,
   "text": "Why not put these into the new block_validity util module?"
  },
  {
   "t": "2026-04-22T10:26:12Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "CONTRIBUTOR",
   "text": "Approach ACK\n\nThis looks like a good place to start - having all these cases is valuable. I think the current scope is fine, but I would call these `TestBlockValidity` unit tests, and not \"block validation\". These skip some checks that run in the `Accept*` functions and don't necessarily reflect the validation steps executed by the actual hot validation path. As far as I can tell the tests here don't exercise `BLOCK_MISSING_PREV` and `BLOCK_INVALID_PREV`.\n\nCan we add the same checks through `ProcessNewBlock` by subscribing to the `BlockChecked` validation interface event?\n\nAs an addition I would also be interested in calling `ConnectBlock` directly with these invalid blocks. I think that would require some re-plumbing though to catch the various errors and validation states.\n\nThe distinction into the separate validation classes makes sense to me. Instead of splitting them up into separate tests I am wondering if we could tag them with an enum and then pass them to various entry points of validation (i.e. TestBlockValidity, ProcessNewBlock, ConnectBlock). The tests can then react to their failures based on their assigned validation variant, i.e. for contextual checks ConnectBlock does not expect failures, but other validation functions do."
  },
  {
   "t": "2026-04-23T12:22:39Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "5ae4c0cc393405176c3a3454bf5ae6ad4fb4fa23"
  },
  {
   "t": "2026-04-23T12:36:50Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/test/validation_block_header_tests.cpp",
   "commit": "26b1e0b554dcb9b6b28ef33e69eefc9c4e34fbdb",
   "in_reply_to": 3123126123,
   "text": "The first attempt used that approach, but I was using the same setup in the fuzz harness, which does not want boost headers included. Now that the fuzz harness is gone and we will likely use setup in #34651 for that it's okay to move the util there."
  },
  {
   "t": "2026-04-28T18:25:03Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "8e556cdf59db04bb604554e7fb9b8a2ce345f2a3"
  },
  {
   "t": "2026-04-28T18:56:37Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/test/validation_block_header_tests.cpp",
   "commit": "26b1e0b554dcb9b6b28ef33e69eefc9c4e34fbdb",
   "in_reply_to": 3123126123,
   "text": "The error I initially saw persists because the fuzz binary includes the test util library, which in turn includes this block_validity.cpp which includes the boost headers. As a result, CI fails https://github.com/bitcoin/bitcoin/actions/runs/24834952407/job/72771891921.\n\nI contemplated extracting `CheckBlockValid` and `CheckBlockInvalid` from the block_validity util, but not sure where it belongs, common?\n\nIn the meantime, I fixed this by not adding the include to the util library and instead including it in test_bitcoin.\nHowever, I\u2019m not happy with this fix. It feels off that a file inside the util directory is not part of the test util library.\n\nwdyt?"
  },
  {
   "t": "2026-04-28T18:57:53Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "I appreciate the review so far. I will spend some time to complete this by incorporating more tests beyond `TestBlockValidity`. In the meantime, I will move this to draft."
  },
  {
   "t": "2026-04-28T19:10:36Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/validation_block_header_tests.cpp",
   "commit": "26b1e0b554dcb9b6b28ef33e69eefc9c4e34fbdb",
   "in_reply_to": 3123126123,
   "text": "Oh, right. Yeah, in that case duplication seems like the only sane way to do this. Or just consolidate all the tests into a single module."
  },
  {
   "t": "2026-05-27T14:15:17Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "0d73f0d9648b0b83ff4966309c510942df050599"
  },
  {
   "t": "2026-05-28T14:43:58Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "c9d6eb5d9d1db6d6c0ffc9096932d40b121b2875"
  },
  {
   "t": "2026-05-28T17:01:28Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "I forced pushed from 8e556cdf59db04bb604554e7fb9b8a2ce345f2a3 to  c9d6eb5d9d1db6d6c0ffc9096932d40b121b2875 [compare_diff](https://github.com/bitcoin/bitcoin/compare/8e556cdf59db04bb604554e7fb9b8a2ce345f2a3..c9d6eb5d9d1db6d6c0ffc9096932d40b121b2875)\n\n- Rebased to fix merge conflict\n- https://github.com/bitcoin/bitcoin/pull/35000#discussion_r3156571901 fixed now\n[quoted text omitted]\n\nThese tests are added now\n\n[quoted text omitted]\nDone in the recent push.\n\n[quoted text omitted]\nHmm, won't this be more involved? I would like to keep it simple as it is currently, if you don't feel strongly about this.\n\nAlso, I limit this to just `TestBlockValidity`, `ProcessNewBlock` and `ConnectBlock`. I think this suffices for now;\nThe block validation parts `VerifyDB` and `ConnectTip` just call `ConnectBlock` anyway, so independent calls of connectBlock added recently due to @sedited suggestion should cover them."
  },
  {
   "t": "2026-06-04T00:20:21Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/validation_block_contextual_tests.cpp",
   "commit": "ccfc34482d1120aa05d65c83d3e41f8dbee851b3",
   "in_reply_to": null,
   "text": "nit:\n`BOOST_REQUIRE(outpoint)` seems to be more appropriate than `if (!outpoint) return` because the outpoint is required for the rest of the test.\nIf helper setup fails, the test will fail immediately instead of silently passing by returning early.\n\n  diff 1\n\n```diff\ndiff --git a/src/test/validation_block_contextual_tests.cpp b/src/test/validation_block_contextual_tests.cpp\nindex 380218b00e..f4c41a9464 100644\n--- a/src/test/validation_block_contextual_tests.cpp\n+++ b/src/test/validation_block_contextual_tests.cpp\n@@ -72,7 +72,7 @@ BOOST_AUTO_TEST_CASE(tbv_bad_txns_nonfinal)\n     // Transactions with time-locks (nLockTime) that are not yet satisfied are rejected.\n     CBlock block = MakeBlock();\n     const auto outpoint = AddCoin(CScript() << OP_TRUE, 50 * COIN);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -98,7 +98,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip113_locktime_uses_mtp)\n     CBlock block = MakeBlock();\n     const int64_t mtp = WITH_LOCK(cs_main, return m_chainstate.m_chain.Tip()->GetMedianTimePast());\n     const auto outpoint = AddCoin(CScript() << OP_TRUE, 50 * COIN);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -139,7 +139,7 @@ BOOST_AUTO_TEST_CASE(tbv_bad_blk_weight)\n     const int num_overweight_txns{4};\n     for (int tx_idx = 0; tx_idx < num_overweight_txns; ++tx_idx) {\n         const auto outpoint = AddCoin(p2tr, 50 * COIN);\n-        if (!outpoint) return;\n+        BOOST_REQUIRE(outpoint);\n         CMutableTransaction mtx;\n         mtx.vin.resize(1);\n         mtx.vin[0].prevout = *outpoint;\ndiff --git a/src/test/validation_block_stateful_tests.cpp b/src/test/validation_block_stateful_tests.cpp\nindex 95eb6285e2..13a42dad92 100644\n--- a/src/test/validation_block_stateful_tests.cpp\n+++ b/src/test/validation_block_stateful_tests.cpp\n@@ -24,7 +24,8 @@ BOOST_AUTO_TEST_CASE(tbv_bad_txns_accumulated_fee_outofrange)\n     CBlock block = MakeBlock();\n     const auto outpoint1 = AddCoin(CScript(), MAX_MONEY);\n     const auto outpoint2 = AddCoin(CScript(), 1);\n-    if (!outpoint1 || !outpoint2) return;\n+    BOOST_REQUIRE(outpoint1);\n+    BOOST_REQUIRE(outpoint2);\n     CMutableTransaction mtx, mtx2;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint1;\n@@ -115,7 +116,8 @@ BOOST_AUTO_TEST_CASE(tbv_bad_txns_inputs_sum_overflow)\n     CScript script = CScript() << OP_TRUE;\n     auto outpoint1 = AddCoin(script, MAX_MONEY);\n     auto outpoint2 = AddCoin(script, MAX_MONEY);\n-    if (!outpoint1 || !outpoint2) return;\n+    BOOST_REQUIRE(outpoint1);\n+    BOOST_REQUIRE(outpoint2);\n     CMutableTransaction mtx;\n     mtx.vin.resize(2);\n     mtx.vin[0].prevout = outpoint1.value();\n@@ -138,7 +140,7 @@ BOOST_AUTO_TEST_CASE(tbv_block_script_verify_flag_failed)\n     CBlock block = MakeBlock();\n     const CScript p2pk = CScript() << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG;\n     const auto outpoint = AddCoin(p2pk, 50 * COIN);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -160,7 +162,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip147_null_dummy)\n     CBlock block = MakeBlock();\n     CScript multisig = CScript() << 1 << ToByteVector(coinbaseKey.GetPubKey()) << 1 << OP_CHECKMULTISIG;\n     const auto outpoint = AddCoin(multisig);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -186,7 +188,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip66_non_der_sig)\n     CBlock block = MakeBlock();\n     CScript p2pkh = GetScriptForDestination(PKHash(coinbaseKey.GetPubKey()));\n     const auto outpoint = AddCoin(p2pkh);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -218,7 +220,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip65_cltv_violation)\n     const int lock_height = WITH_LOCK(cs_main, return m_chainstate.m_chain.Height()) + cltv_height_offset;\n     const CScript cltv_script = CScript() << lock_height << OP_CHECKLOCKTIMEVERIFY << OP_DROP << OP_TRUE;\n     const auto outpoint = AddCoin(cltv_script);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -245,7 +247,7 @@ BOOST_AUTO_TEST_CASE(tbv_taproot_invalid_sig)\n     WitnessV1Taproot taproot = builder.GetOutput();\n     CScript p2tr_script = GetScriptForDestination(taproot);\n     const auto outpoint = AddCoin(p2tr_script);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -278,7 +280,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip112_csv_violation)\n     uint32_t csv_block_offset = 10;\n     const CScript csv_script = CScript() << csv_block_offset << OP_CHECKSEQUENCEVERIFY << OP_DROP << OP_TRUE;\n     const auto outpoint = AddCoin(csv_script);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.version = 2; // CSV requires nVersion >= 2\n     mtx.vin.resize(1);\n@@ -303,7 +305,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip16_p2sh_invalid_redeem_script)\n     const CScript redeem_script = CScript() << OP_FALSE;\n     const CScript p2sh = GetScriptForDestination(ScriptHash(redeem_script));\n     const auto outpoint = AddCoin(p2sh);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -326,7 +328,7 @@ BOOST_AUTO_TEST_CASE(tbv_segwit_v0_invalid_witness_script)\n     const CScript witness_script = CScript() << OP_FALSE;\n     const CScript p2wsh = GetScriptForDestination(WitnessV0ScriptHash(witness_script));\n     const auto outpoint = AddCoin(p2wsh);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -357,7 +359,7 @@ BOOST_AUTO_TEST_CASE(tbv_tapscript_invalid_script_path)\n     WitnessV1Taproot taproot = builder.GetOutput();\n     const CScript p2tr_script = GetScriptForDestination(taproot);\n     const auto outpoint = AddCoin(p2tr_script);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     // Build the control block for the script path\n     const auto spend_data = builder.GetSpendData();\n     const auto& control_block = *spend_data.scripts.at(\n@@ -390,7 +392,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip143_wrong_amount_in_sighash)\n     const CScript p2wpkh = GetScriptForDestination(WitnessV0KeyHash(coinbaseKey.GetPubKey()));\n     const CAmount actual_value = 1 * COIN;\n     const auto outpoint = AddCoin(p2wpkh, actual_value);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -418,7 +420,7 @@ BOOST_AUTO_TEST_CASE(tbv_empty_scriptsig)\n     // A non-coinbase transaction with an empty scriptSig spending OP_TRUE is valid.\n     CBlock block = MakeBlock();\n     const auto outpoint = AddCoin(CScript() << OP_TRUE);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n@@ -439,7 +441,7 @@ BOOST_AUTO_TEST_CASE(tbv_scriptsig_non_push)\n     // A scriptSig with non-push opcodes is valid in a block (consensus) even if non-standard.\n     CBlock block = MakeBlock();\n     const auto outpoint = AddCoin(CScript() << OP_TRUE);\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n@@ -459,7 +461,7 @@ BOOST_AUTO_TEST_CASE(tbv_double_spend_same_block)\n     // A block containing two transactions spending the same UTXO is invalid.\n     CBlock block = MakeBlock();\n     const auto outpoint = AddCoin();\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     for (int tx_idx = 0; tx_idx < 2; ++tx_idx) {\n         CMutableTransaction mtx;\n         mtx.vin.resize(1);\n@@ -483,7 +485,7 @@ BOOST_AUTO_TEST_CASE(tbv_zero_value_output)\n     // A transaction with a zero-value output is consensus-valid.\n     CBlock block = MakeBlock();\n     const auto outpoint = AddCoin();\n-    if (!outpoint) return;\n+    BOOST_REQUIRE(outpoint);\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n```\n\nAlternatively, this can be centralized in `AddCoin()` itself by making it return `COutPoint` directly and fail internally if no unused outpoint can be found.\n\n  diff 2\n\n```diff\ndiff --git a/src/test/util/block_validity.cpp b/src/test/util/block_validity.cpp\nindex 289339d81c..dfc314b4ae 100644\n--- a/src/test/util/block_validity.cpp\n+++ b/src/test/util/block_validity.cpp\n@@ -29,7 +29,7 @@ BlockValidationState ValidationBlockValidityTestingSetup::TestValidity(CBlock& b\n     return TestBlockValidity(m_chainstate, block, check_pow, check_merkle);\n }\n\n-std::optional<COutPoint> ValidationBlockValidityTestingSetup::AddCoin(const CScript& script_pub_key, CAmount amount)\n+COutPoint ValidationBlockValidityTestingSetup::AddCoin(const CScript& script_pub_key, CAmount amount)\n {\n     // Max trials to find a unique random outpoint\n     const int max_trials{5};\n@@ -41,7 +41,8 @@ std::optional<COutPoint> ValidationBlockValidityTestingSetup::AddCoin(const CScr\n             return outpoint;\n         }\n     }\n-    return std::nullopt;\n+    BOOST_REQUIRE_MESSAGE(false, \"AddCoin: failed to find an unused outpoint\");\n+    return {};\n }\n\n void ValidationBlockValidityTestingSetup::SolveBlockPoW(CBlock& block)\ndiff --git a/src/test/util/block_validity.h b/src/test/util/block_validity.h\nindex 90b1aaf763..db6d5c6f8b 100644\n--- a/src/test/util/block_validity.h\n+++ b/src/test/util/block_validity.h\n@@ -10,8 +10,6 @@\n #include <validation.h>\n #include <validationinterface.h>\n\n-#include <optional>\n-\n /**\n  * A CValidationInterface subscriber that records the BlockValidationState from\n  * each BlockChecked notification. Use ClearCheckedBlockStates() to reset\n@@ -63,7 +61,7 @@ struct ValidationBlockValidityTestingSetup : public TestChain100Setup {\n     BlockValidationState ConnectBlock(CBlock& block);\n\n     /** Helper to add a spendable coin to the UTXO set for testing. */\n-    std::optional<COutPoint> AddCoin(const CScript& script_pub_key = CScript() << OP_TRUE, CAmount amount = 1 * COIN);\n+    COutPoint AddCoin(const CScript& script_pub_key = CScript() << OP_TRUE, CAmount amount = 1 * COIN);\n     /**\n      * Submit a block via ProcessNewBlock, drain the validation signal queue,\n      * and return the single BlockChecked notification state.\ndiff --git a/src/test/validation_block_contextual_tests.cpp b/src/test/validation_block_contextual_tests.cpp\nindex 380218b00e..5f59193870 100644\n--- a/src/test/validation_block_contextual_tests.cpp\n+++ b/src/test/validation_block_contextual_tests.cpp\n@@ -72,10 +72,9 @@ BOOST_AUTO_TEST_CASE(tbv_bad_txns_nonfinal)\n     // Transactions with time-locks (nLockTime) that are not yet satisfied are rejected.\n     CBlock block = MakeBlock();\n     const auto outpoint = AddCoin(CScript() << OP_TRUE, 50 * COIN);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vin[0].nSequence = 0;\n     mtx.vout.resize(1);\n     mtx.vout[0] = CTxOut(49 * COIN, CScript() << OP_TRUE);\n@@ -98,10 +97,9 @@ BOOST_AUTO_TEST_CASE(tbv_bip113_locktime_uses_mtp)\n     CBlock block = MakeBlock();\n     const int64_t mtp = WITH_LOCK(cs_main, return m_chainstate.m_chain.Tip()->GetMedianTimePast());\n     const auto outpoint = AddCoin(CScript() << OP_TRUE, 50 * COIN);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 49 * COIN;\n     // nLockTime exactly equal to MTP: non-final because IsFinalTx requires strictly less than MTP\n@@ -139,10 +137,9 @@ BOOST_AUTO_TEST_CASE(tbv_bad_blk_weight)\n     const int num_overweight_txns{4};\n     for (int tx_idx = 0; tx_idx < num_overweight_txns; ++tx_idx) {\n         const auto outpoint = AddCoin(p2tr, 50 * COIN);\n-        if (!outpoint) return;\n         CMutableTransaction mtx;\n         mtx.vin.resize(1);\n-        mtx.vin[0].prevout = *outpoint;\n+        mtx.vin[0].prevout = outpoint;\n         mtx.vout.resize(1);\n         mtx.vout[0].nValue = 49 * COIN;\n         // 1MB witness item, allowed by OP_SUCCESS bypassing the 520-byte limit\ndiff --git a/src/test/validation_block_stateful_tests.cpp b/src/test/validation_block_stateful_tests.cpp\nindex 95eb6285e2..cd36ee377f 100644\n--- a/src/test/validation_block_stateful_tests.cpp\n+++ b/src/test/validation_block_stateful_tests.cpp\n@@ -24,14 +24,13 @@ BOOST_AUTO_TEST_CASE(tbv_bad_txns_accumulated_fee_outofrange)\n     CBlock block = MakeBlock();\n     const auto outpoint1 = AddCoin(CScript(), MAX_MONEY);\n     const auto outpoint2 = AddCoin(CScript(), 1);\n-    if (!outpoint1 || !outpoint2) return;\n     CMutableTransaction mtx, mtx2;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint1;\n+    mtx.vin[0].prevout = outpoint1;\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 0;\n     mtx2.vin.resize(1);\n-    mtx2.vin[0].prevout = *outpoint2;\n+    mtx2.vin[0].prevout = outpoint2;\n     mtx2.vout.resize(1);\n     mtx2.vout[0].nValue = 0;\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n@@ -115,11 +114,10 @@ BOOST_AUTO_TEST_CASE(tbv_bad_txns_inputs_sum_overflow)\n     CScript script = CScript() << OP_TRUE;\n     auto outpoint1 = AddCoin(script, MAX_MONEY);\n     auto outpoint2 = AddCoin(script, MAX_MONEY);\n-    if (!outpoint1 || !outpoint2) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(2);\n-    mtx.vin[0].prevout = outpoint1.value();\n-    mtx.vin[1].prevout = outpoint2.value();\n+    mtx.vin[0].prevout = outpoint1;\n+    mtx.vin[1].prevout = outpoint2;\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = MAX_MONEY;\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n@@ -138,20 +136,19 @@ BOOST_AUTO_TEST_CASE(tbv_block_script_verify_flag_failed)\n     CBlock block = MakeBlock();\n     const CScript p2pk = CScript() << ToByteVector(coinbaseKey.GetPubKey()) << OP_CHECKSIG;\n     const auto outpoint = AddCoin(p2pk, 50 * COIN);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vin[0].scriptSig = CScript() << OP_0;\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 49 * COIN;\n     block.vtx.emplace_back(MakeTransactionRef(std::move(mtx)));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)\";\n-    CheckScriptViolation(TestValidity(block), block, *outpoint, reason);\n-    CheckScriptViolation(ConnectBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(TestValidity(block), block, outpoint, reason);\n+    CheckScriptViolation(ConnectBlock(block), block, outpoint, reason);\n     SolveBlockPoW(block);\n-    CheckScriptViolation(ProcessNewBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(ProcessNewBlock(block), block, outpoint, reason);\n }\n\n BOOST_AUTO_TEST_CASE(tbv_bip147_null_dummy)\n@@ -160,10 +157,9 @@ BOOST_AUTO_TEST_CASE(tbv_bip147_null_dummy)\n     CBlock block = MakeBlock();\n     CScript multisig = CScript() << 1 << ToByteVector(coinbaseKey.GetPubKey()) << 1 << OP_CHECKMULTISIG;\n     const auto outpoint = AddCoin(multisig);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 0.9 * COIN;\n     std::vector<unsigned char> sig;\n@@ -174,10 +170,10 @@ BOOST_AUTO_TEST_CASE(tbv_bip147_null_dummy)\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Dummy CHECKMULTISIG argument must be zero)\";\n-    CheckScriptViolation(TestValidity(block), block, *outpoint, reason);\n-    CheckScriptViolation(ConnectBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(TestValidity(block), block, outpoint, reason);\n+    CheckScriptViolation(ConnectBlock(block), block, outpoint, reason);\n     SolveBlockPoW(block);\n-    CheckScriptViolation(ProcessNewBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(ProcessNewBlock(block), block, outpoint, reason);\n }\n\n BOOST_AUTO_TEST_CASE(tbv_bip66_non_der_sig)\n@@ -186,10 +182,9 @@ BOOST_AUTO_TEST_CASE(tbv_bip66_non_der_sig)\n     CBlock block = MakeBlock();\n     CScript p2pkh = GetScriptForDestination(PKHash(coinbaseKey.GetPubKey()));\n     const auto outpoint = AddCoin(p2pkh);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 0.9 * COIN;\n     uint256 hash = SignatureHash(p2pkh, mtx, 0, SIGHASH_ALL, 1 * COIN, SigVersion::BASE);\n@@ -202,10 +197,10 @@ BOOST_AUTO_TEST_CASE(tbv_bip66_non_der_sig)\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Non-canonical DER signature)\";\n-    CheckScriptViolation(TestValidity(block), block, *outpoint, reason);\n-    CheckScriptViolation(ConnectBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(TestValidity(block), block, outpoint, reason);\n+    CheckScriptViolation(ConnectBlock(block), block, outpoint, reason);\n     SolveBlockPoW(block);\n-    CheckScriptViolation(ProcessNewBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(ProcessNewBlock(block), block, outpoint, reason);\n }\n\n BOOST_AUTO_TEST_CASE(tbv_bip65_cltv_violation)\n@@ -218,10 +213,9 @@ BOOST_AUTO_TEST_CASE(tbv_bip65_cltv_violation)\n     const int lock_height = WITH_LOCK(cs_main, return m_chainstate.m_chain.Height()) + cltv_height_offset;\n     const CScript cltv_script = CScript() << lock_height << OP_CHECKLOCKTIMEVERIFY << OP_DROP << OP_TRUE;\n     const auto outpoint = AddCoin(cltv_script);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vin[0].nSequence = CTxIn::SEQUENCE_FINAL; // Makes IsFinalTx pass, but disables CLTV\n     mtx.nLockTime = 0;\n     mtx.vout.resize(1);\n@@ -229,10 +223,10 @@ BOOST_AUTO_TEST_CASE(tbv_bip65_cltv_violation)\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Locktime requirement not satisfied)\";\n-    CheckScriptViolation(TestValidity(block), block, *outpoint, reason);\n-    CheckScriptViolation(ConnectBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(TestValidity(block), block, outpoint, reason);\n+    CheckScriptViolation(ConnectBlock(block), block, outpoint, reason);\n     SolveBlockPoW(block);\n-    CheckScriptViolation(ProcessNewBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(ProcessNewBlock(block), block, outpoint, reason);\n }\n\n BOOST_AUTO_TEST_CASE(tbv_taproot_invalid_sig)\n@@ -245,10 +239,9 @@ BOOST_AUTO_TEST_CASE(tbv_taproot_invalid_sig)\n     WitnessV1Taproot taproot = builder.GetOutput();\n     CScript p2tr_script = GetScriptForDestination(taproot);\n     const auto outpoint = AddCoin(p2tr_script);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 0.9 * COIN;\n     // Create the Taproot signature (Keypath spend)\n@@ -264,10 +257,10 @@ BOOST_AUTO_TEST_CASE(tbv_taproot_invalid_sig)\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Invalid Schnorr signature)\";\n-    CheckScriptViolation(TestValidity(block), block, *outpoint, reason);\n-    CheckScriptViolation(ConnectBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(TestValidity(block), block, outpoint, reason);\n+    CheckScriptViolation(ConnectBlock(block), block, outpoint, reason);\n     SolveBlockPoW(block);\n-    CheckScriptViolation(ProcessNewBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(ProcessNewBlock(block), block, outpoint, reason);\n }\n\n BOOST_AUTO_TEST_CASE(tbv_bip112_csv_violation)\n@@ -278,21 +271,20 @@ BOOST_AUTO_TEST_CASE(tbv_bip112_csv_violation)\n     uint32_t csv_block_offset = 10;\n     const CScript csv_script = CScript() << csv_block_offset << OP_CHECKSEQUENCEVERIFY << OP_DROP << OP_TRUE;\n     const auto outpoint = AddCoin(csv_script);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.version = 2; // CSV requires nVersion >= 2\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vin[0].nSequence = csv_block_offset - 1; // One block short of the requirement\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 0.9 * COIN;\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Locktime requirement not satisfied)\";\n-    CheckScriptViolation(TestValidity(block), block, *outpoint, reason);\n-    CheckScriptViolation(ConnectBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(TestValidity(block), block, outpoint, reason);\n+    CheckScriptViolation(ConnectBlock(block), block, outpoint, reason);\n     SolveBlockPoW(block);\n-    CheckScriptViolation(ProcessNewBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(ProcessNewBlock(block), block, outpoint, reason);\n }\n\n BOOST_AUTO_TEST_CASE(tbv_bip16_p2sh_invalid_redeem_script)\n@@ -303,20 +295,19 @@ BOOST_AUTO_TEST_CASE(tbv_bip16_p2sh_invalid_redeem_script)\n     const CScript redeem_script = CScript() << OP_FALSE;\n     const CScript p2sh = GetScriptForDestination(ScriptHash(redeem_script));\n     const auto outpoint = AddCoin(p2sh);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vin[0].scriptSig = CScript() << std::vector<unsigned char>(redeem_script.begin(), redeem_script.end());\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 0.9 * COIN;\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)\";\n-    CheckScriptViolation(TestValidity(block), block, *outpoint, reason);\n-    CheckScriptViolation(ConnectBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(TestValidity(block), block, outpoint, reason);\n+    CheckScriptViolation(ConnectBlock(block), block, outpoint, reason);\n     SolveBlockPoW(block);\n-    CheckScriptViolation(ProcessNewBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(ProcessNewBlock(block), block, outpoint, reason);\n }\n\n BOOST_AUTO_TEST_CASE(tbv_segwit_v0_invalid_witness_script)\n@@ -326,10 +317,9 @@ BOOST_AUTO_TEST_CASE(tbv_segwit_v0_invalid_witness_script)\n     const CScript witness_script = CScript() << OP_FALSE;\n     const CScript p2wsh = GetScriptForDestination(WitnessV0ScriptHash(witness_script));\n     const auto outpoint = AddCoin(p2wsh);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     // Witness: <witness_script>\n     mtx.vin[0].scriptWitness.stack = {\n         std::vector<unsigned char>(witness_script.begin(), witness_script.end())};\n@@ -338,10 +328,10 @@ BOOST_AUTO_TEST_CASE(tbv_segwit_v0_invalid_witness_script)\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)\";\n-    CheckScriptViolation(TestValidity(block), block, *outpoint, reason);\n-    CheckScriptViolation(ConnectBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(TestValidity(block), block, outpoint, reason);\n+    CheckScriptViolation(ConnectBlock(block), block, outpoint, reason);\n     SolveBlockPoW(block);\n-    CheckScriptViolation(ProcessNewBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(ProcessNewBlock(block), block, outpoint, reason);\n }\n\n BOOST_AUTO_TEST_CASE(tbv_tapscript_invalid_script_path)\n@@ -357,7 +347,6 @@ BOOST_AUTO_TEST_CASE(tbv_tapscript_invalid_script_path)\n     WitnessV1Taproot taproot = builder.GetOutput();\n     const CScript p2tr_script = GetScriptForDestination(taproot);\n     const auto outpoint = AddCoin(p2tr_script);\n-    if (!outpoint) return;\n     // Build the control block for the script path\n     const auto spend_data = builder.GetSpendData();\n     const auto& control_block = *spend_data.scripts.at(\n@@ -365,7 +354,7 @@ BOOST_AUTO_TEST_CASE(tbv_tapscript_invalid_script_path)\n                                      .begin();\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     // Witness stack for script path: <leaf_script> <control_block> (OP_FALSE consumes no args)\n     mtx.vin[0].scriptWitness.stack = {\n         std::vector<unsigned char>(leaf_script.begin(), leaf_script.end()),\n@@ -375,10 +364,10 @@ BOOST_AUTO_TEST_CASE(tbv_tapscript_invalid_script_path)\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)\";\n-    CheckScriptViolation(TestValidity(block), block, *outpoint, reason);\n-    CheckScriptViolation(ConnectBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(TestValidity(block), block, outpoint, reason);\n+    CheckScriptViolation(ConnectBlock(block), block, outpoint, reason);\n     SolveBlockPoW(block);\n-    CheckScriptViolation(ProcessNewBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(ProcessNewBlock(block), block, outpoint, reason);\n }\n\n BOOST_AUTO_TEST_CASE(tbv_bip143_wrong_amount_in_sighash)\n@@ -390,10 +379,9 @@ BOOST_AUTO_TEST_CASE(tbv_bip143_wrong_amount_in_sighash)\n     const CScript p2wpkh = GetScriptForDestination(WitnessV0KeyHash(coinbaseKey.GetPubKey()));\n     const CAmount actual_value = 1 * COIN;\n     const auto outpoint = AddCoin(p2wpkh, actual_value);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 0.9 * COIN;\n     // Sign over a wrong amount \u2014 this is the BIP143-specific failure\n@@ -407,10 +395,10 @@ BOOST_AUTO_TEST_CASE(tbv_bip143_wrong_amount_in_sighash)\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)\";\n-    CheckScriptViolation(TestValidity(block), block, *outpoint, reason);\n-    CheckScriptViolation(ConnectBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(TestValidity(block), block, outpoint, reason);\n+    CheckScriptViolation(ConnectBlock(block), block, outpoint, reason);\n     SolveBlockPoW(block);\n-    CheckScriptViolation(ProcessNewBlock(block), block, *outpoint, reason);\n+    CheckScriptViolation(ProcessNewBlock(block), block, outpoint, reason);\n }\n\n BOOST_AUTO_TEST_CASE(tbv_empty_scriptsig)\n@@ -418,11 +406,10 @@ BOOST_AUTO_TEST_CASE(tbv_empty_scriptsig)\n     // A non-coinbase transaction with an empty scriptSig spending OP_TRUE is valid.\n     CBlock block = MakeBlock();\n     const auto outpoint = AddCoin(CScript() << OP_TRUE);\n-    if (!outpoint) return;\n\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vin[0].scriptSig = CScript();\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 1;\n@@ -439,10 +426,9 @@ BOOST_AUTO_TEST_CASE(tbv_scriptsig_non_push)\n     // A scriptSig with non-push opcodes is valid in a block (consensus) even if non-standard.\n     CBlock block = MakeBlock();\n     const auto outpoint = AddCoin(CScript() << OP_TRUE);\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vin[0].scriptSig = CScript() << OP_1 << OP_DROP;\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 1;\n@@ -459,11 +445,10 @@ BOOST_AUTO_TEST_CASE(tbv_double_spend_same_block)\n     // A block containing two transactions spending the same UTXO is invalid.\n     CBlock block = MakeBlock();\n     const auto outpoint = AddCoin();\n-    if (!outpoint) return;\n     for (int tx_idx = 0; tx_idx < 2; ++tx_idx) {\n         CMutableTransaction mtx;\n         mtx.vin.resize(1);\n-        mtx.vin[0].prevout = *outpoint;\n+        mtx.vin[0].prevout = outpoint;\n         mtx.vin[0].scriptSig = CScript() << OP_TRUE;\n         mtx.vout.resize(1);\n         mtx.vout[0].nValue = 1;\n@@ -483,10 +468,9 @@ BOOST_AUTO_TEST_CASE(tbv_zero_value_output)\n     // A transaction with a zero-value output is consensus-valid.\n     CBlock block = MakeBlock();\n     const auto outpoint = AddCoin();\n-    if (!outpoint) return;\n     CMutableTransaction mtx;\n     mtx.vin.resize(1);\n-    mtx.vin[0].prevout = *outpoint;\n+    mtx.vin[0].prevout = outpoint;\n     mtx.vin[0].scriptSig = CScript() << OP_TRUE;\n     mtx.vout.resize(1);\n     mtx.vout[0].nValue = 0;\n```"
  },
  {
   "t": "2026-06-04T01:09:26Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/validation_block_stateful_tests.cpp",
   "commit": "c9d6eb5d9d1db6d6c0ffc9096932d40b121b2875",
   "in_reply_to": null,
   "text": "micro-nit:\n\n`0.9 * COIN` uses floating-point arithmetic.\n\n`COIN * 9 / 10` is evaluated as `(COIN * 9) / 10`, using integer arithmetic throughout, which fits `CAmount` better.\n\n  diff\n\n```diff\ndiff --git a/src/test/validation_block_stateful_tests.cpp b/src/test/validation_block_stateful_tests.cpp\nindex 95eb6285e2..71a59df65b 100644\n--- a/src/test/validation_block_stateful_tests.cpp\n+++ b/src/test/validation_block_stateful_tests.cpp\n@@ -165,7 +165,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip147_null_dummy)\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n     mtx.vout.resize(1);\n-    mtx.vout[0].nValue = 0.9 * COIN;\n+    mtx.vout[0].nValue = COIN * 9 / 10;\n     std::vector<unsigned char> sig;\n     uint256 hash = SignatureHash(multisig, mtx, 0, SIGHASH_ALL, 1 * COIN, SigVersion::BASE);\n     BOOST_REQUIRE(coinbaseKey.Sign(hash, sig));\n@@ -191,7 +191,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip66_non_der_sig)\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n     mtx.vout.resize(1);\n-    mtx.vout[0].nValue = 0.9 * COIN;\n+    mtx.vout[0].nValue = COIN * 9 / 10;\n     uint256 hash = SignatureHash(p2pkh, mtx, 0, SIGHASH_ALL, 1 * COIN, SigVersion::BASE);\n     std::vector<unsigned char> sig;\n     BOOST_REQUIRE(coinbaseKey.Sign(hash, sig));\n@@ -225,7 +225,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip65_cltv_violation)\n     mtx.vin[0].nSequence = CTxIn::SEQUENCE_FINAL; // Makes IsFinalTx pass, but disables CLTV\n     mtx.nLockTime = 0;\n     mtx.vout.resize(1);\n-    mtx.vout[0].nValue = 0.9 * COIN;\n+    mtx.vout[0].nValue = COIN * 9 / 10;\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Locktime requirement not satisfied)\";\n@@ -250,7 +250,7 @@ BOOST_AUTO_TEST_CASE(tbv_taproot_invalid_sig)\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n     mtx.vout.resize(1);\n-    mtx.vout[0].nValue = 0.9 * COIN;\n+    mtx.vout[0].nValue = COIN * 9 / 10;\n     // Create the Taproot signature (Keypath spend)\n     FlatSigningProvider provider;\n     provider.keys[coinbaseKey.GetPubKey().GetID()] = coinbaseKey;\n@@ -285,7 +285,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip112_csv_violation)\n     mtx.vin[0].prevout = *outpoint;\n     mtx.vin[0].nSequence = csv_block_offset - 1; // One block short of the requirement\n     mtx.vout.resize(1);\n-    mtx.vout[0].nValue = 0.9 * COIN;\n+    mtx.vout[0].nValue = COIN * 9 / 10;\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Locktime requirement not satisfied)\";\n@@ -309,7 +309,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip16_p2sh_invalid_redeem_script)\n     mtx.vin[0].prevout = *outpoint;\n     mtx.vin[0].scriptSig = CScript() << std::vector<unsigned char>(redeem_script.begin(), redeem_script.end());\n     mtx.vout.resize(1);\n-    mtx.vout[0].nValue = 0.9 * COIN;\n+    mtx.vout[0].nValue = COIN * 9 / 10;\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)\";\n@@ -334,7 +334,7 @@ BOOST_AUTO_TEST_CASE(tbv_segwit_v0_invalid_witness_script)\n     mtx.vin[0].scriptWitness.stack = {\n         std::vector<unsigned char>(witness_script.begin(), witness_script.end())};\n     mtx.vout.resize(1);\n-    mtx.vout[0].nValue = 0.9 * COIN;\n+    mtx.vout[0].nValue = COIN * 9 / 10;\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)\";\n@@ -371,7 +371,7 @@ BOOST_AUTO_TEST_CASE(tbv_tapscript_invalid_script_path)\n         std::vector<unsigned char>(leaf_script.begin(), leaf_script.end()),\n         control_block};\n     mtx.vout.resize(1);\n-    mtx.vout[0].nValue = 0.9 * COIN;\n+    mtx.vout[0].nValue = COIN * 9 / 10;\n     block.vtx.emplace_back(MakeTransactionRef(mtx));\n     RegenerateCommitments(block, *m_node.chainman);\n     const auto reason = \"block-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)\";\n@@ -395,7 +395,7 @@ BOOST_AUTO_TEST_CASE(tbv_bip143_wrong_amount_in_sighash)\n     mtx.vin.resize(1);\n     mtx.vin[0].prevout = *outpoint;\n     mtx.vout.resize(1);\n-    mtx.vout[0].nValue = 0.9 * COIN;\n+    mtx.vout[0].nValue = COIN * 9 / 10;\n     // Sign over a wrong amount \u2014 this is the BIP143-specific failure\n     const CAmount wrong_value = actual_value + 1;\n     const CScript p2wpkh_script = GetScriptForDestination(PKHash(coinbaseKey.GetPubKey()));\n```"
  },
  {
   "t": "2026-06-04T01:09:57Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "c9d6eb5d9d1db6d6c0ffc9096932d40b121b2875",
   "text": "ACK c9d6eb5d9d1db6d6c0ffc9096932d40b121b2875\n\nSome non-blocking nits:"
  },
  {
   "t": "2026-06-05T16:53:50Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "ccfc34482d1120aa05d65c83d3e41f8dbee851b3"
  },
  {
   "t": "2026-06-05T16:57:24Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "Forced pushed from  c9d6eb5d9d1db6d6c0ffc9096932d40b121b2875 to ccfc34482d1120aa05d65c83d3e41f8dbee851b3 [compare diff](https://github.com/bitcoin/bitcoin/compare/c9d6eb5d9d1db6d6c0ffc9096932d40b121b2875..ccfc34482d1120aa05d65c83d3e41f8dbee851b3)\n\n- https://github.com/bitcoin/bitcoin/pull/35000#discussion_r3352677834 took the second approach, it's more DRY\n- https://github.com/bitcoin/bitcoin/pull/35000#discussion_r3352838025 taken"
  }
 ],
 "labels_log": [
  {
   "t": "2026-04-04T01:42:29Z",
   "action": "labeled",
   "label": "Tests",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-23T13:09:55Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-28T19:17:29Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-27T15:26:35Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-28T16:55:06Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-04-10T10:46:12Z",
   "kind": "renamed",
   "who": "ismaelsadeeq",
   "from": "test: Add block validation unit tests + fuzz target for `TestBlockValidity`",
   "to": "test: Add block validation unit tests + fuzz harness for `TestBlockValidity`"
  },
  {
   "t": "2026-04-10T16:11:33Z",
   "kind": "renamed",
   "who": "ismaelsadeeq",
   "from": "test: Add block validation unit tests + fuzz harness for `TestBlockValidity`",
   "to": "test: Add block validation unit tests"
  },
  {
   "t": "2026-04-28T18:58:19Z",
   "kind": "convert_to_draft",
   "who": "ismaelsadeeq"
  },
  {
   "t": "2026-05-28T16:55:08Z",
   "kind": "ready_for_review",
   "who": "ismaelsadeeq"
  }
 ],
 "text_chars": 47562,
 "text_tokens_estimate": 11890,
 "changed_paths": [
  "src/test/CMakeLists.txt",
  "src/test/block_validity_checks.h",
  "src/test/util/block_validity.cpp",
  "src/test/util/block_validity.h",
  "src/test/validation_block_contextual_tests.cpp",
  "src/test/validation_block_header_tests.cpp",
  "src/test/validation_block_stateful_tests.cpp",
  "src/test/validation_block_stateless_tests.cpp"
 ],
 "files": [
  {
   "path": "src/test/CMakeLists.txt",
   "add": 5,
   "del": 0
  },
  {
   "path": "src/test/block_validity_checks.h",
   "add": 60,
   "del": 0
  },
  {
   "path": "src/test/util/block_validity.cpp",
   "add": 85,
   "del": 0
  },
  {
   "path": "src/test/util/block_validity.h",
   "add": 72,
   "del": 0
  },
  {
   "path": "src/test/validation_block_contextual_tests.cpp",
   "add": 196,
   "del": 0
  },
  {
   "path": "src/test/validation_block_header_tests.cpp",
   "add": 142,
   "del": 0
  },
  {
   "path": "src/test/validation_block_stateful_tests.cpp",
   "add": 485,
   "del": 0
  },
  {
   "path": "src/test/validation_block_stateless_tests.cpp",
   "add": 339,
   "del": 0
  }
 ],
 "test_lines": 1384,
 "git": {
  "head": "ccfc34482d1120aa05d65c83d3e41f8dbee851b3",
  "head_matches_backup": true,
  "base": "896eaacd9134c460b5e0724f7f18c15964365e40",
  "commits": [
   {
    "sha": "c79663e45e",
    "subject": "test: Add block validity test setup and TestBlockValidity tests",
    "files": 8,
    "add": 1125,
    "del": 0
   },
   {
    "sha": "8527fda363",
    "subject": "test: Add ProcessNewBlock block validation path unit tests",
    "files": 6,
    "add": 178,
    "del": 1
   },
   {
    "sha": "ccfc34482d",
    "subject": "test: Add ConnectBlock block validation path unit tests",
    "files": 6,
    "add": 82,
    "del": 0
   }
  ],
  "patch_truncated": true
 },
 "input_hash": "001012387e47777f",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}