{
 "number": 36109,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36109",
 "title": "test: harden arbitrary-parent block creation",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-08-28T00:17:31Z",
 "updated_at": "2026-09-16T12:11:50Z",
 "age_days": 20,
 "draft": false,
 "labels": [
  "Tests"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "6052be3135c35573017263509734476ed43eb975",
 "head_ref": "l0rinc/test-build-valid-fork-blocks",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [],
 "additions": 32,
 "deletions": 15,
 "changed_files": 3,
 "commit_count": 4,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "jeanpablojp",
      "url": "https://github.com/bitcoin/bitcoin/pull/36109#pullrequestreview-5145514356"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36244,
     "title": "validation, net: Process blocks asynchronously and reduce cs_main contention",
     "author": "w0xlt"
    },
    {
     "number": 35675,
     "title": "mining: add block template manager",
     "author": "ismaelsadeeq"
    },
    {
     "number": 35581,
     "title": "node: add block template manager and track waitNext fee inflow",
     "author": "ismaelsadeeq"
    },
    {
     "number": 33922,
     "title": "mining: add getMemoryLoad() and track template non-mempool memory footprint",
     "author": "Sjors"
    }
   ]
  }
 },
 "acks_parsed": {
  "jeanpablojp": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-08T18:27:14Z",
   "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": 0,
  "distinct_reviewers": [
   "jeanpablojp"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-08-28T00:08:57Z",
  "last_reviewer_activity": "2026-09-08T18:27:14Z",
  "last_reviewer": "jeanpablojp",
  "author_silent_days": 20,
  "waiting_on_author_days": 8,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   35847
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35847,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-08-14",
    "title": "test: move more tests to `baseindex_tests` and run them for all indexes"
   }
  ],
  "conflicts": [
   36244,
   35675,
   35581,
   33922
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/test/util/mining.cpp"
 ],
 "body": "**Problem:** While reviewing #35847, a few hardening opportunities came up in the test helpers that attach active-tip block templates to arbitrary parents.\nThe copied work target and subsidy may describe a different child, and rebuilding a coinbase input script from heights 1 through 16 without padding violates its minimum length.\nExisting callers already avoid these cases or handle them locally, so no current test fails.\n\n**Fix:** Share the parent-dependent reconstruction between `BuildChain()` and `MinerTestingSetup::Block()`.\nUse the caller-selected timestamp, recalculate the work target and subsidy for the actual parent and height, and append a dummy `OP_0` to low-height coinbase input scripts.\nRequire the validation setup's completed block to retain the reconstructed locktime, script, and subsidy.",
 "commits": [
  {
   "sha": "afcf26f0efb3e797d5d2ce1a041e7d5013484ca3",
   "date": "2026-08-28T00:08:56Z",
   "message": "test: extract fork block reconstruction\n\n`BuildChain` creates a block template for the active tip and then rewrites it for the requested parent.\nMove the existing parent-dependent field rewrites into a helper while passing the existing timestamp explicitly, without changing the generated fields or behavior."
  },
  {
   "sha": "4eee9d52758af4eb5f25389065d6a7a02cf6bb9d",
   "date": "2026-08-28T00:08:56Z",
   "message": "test: derive fork difficulty from parent\n\n`BuildChain` copies the work target from a block template for the active tip before attaching the block to its requested parent.\nRecalculate `nBits` from that parent and the rebuilt header so the target describes the resulting child.\n\nCurrent callers use regtest with retargeting disabled, so this prevents future invalid test cases rather than fixing an existing failure."
  },
  {
   "sha": "b82a725a6c055e330c4f66ad00e735615f820fd4",
   "date": "2026-08-28T00:08:57Z",
   "message": "test: derive fork subsidy from parent\n\n`BuildChain` and `validation_block_tests::MinerTestingSetup::Block` attach active-tip templates to arbitrary parents, but retain the template subsidy.\nDerive the resulting height and subsidy in the shared reconstruction so both builders pay the correct coinbase reward.\n\n`MinerTestingSetup::Block` passes its existing monotonic timestamp into the reconstruction and asserts that the rebuilt coinbase retains the parent-height `nLockTime` and subsidy after customizing its outputs.\n\nCurrent callers do not cross a subsidy halving, so this prevents future invalid test cases rather than fixing an existing failure."
  },
  {
   "sha": "6052be3135c35573017263509734476ed43eb975",
   "date": "2026-08-28T00:08:57Z",
   "message": "test: pad low-height fork coinbases\n\n`RebuildBlockForParent` replaces the template coinbase input script with the child height, which encodes to one byte at heights 1 through 16.\nAppend the dummy `OP_0` in the shared reconstruction and replace the validation helper's duplicate padding with an assertion so both builders satisfy the two-byte minimum."
  }
 ],
 "timeline": [
  {
   "t": "2026-09-08T18:27:14Z",
   "kind": "review",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "6052be3135c35573017263509734476ed43eb975",
   "text": "Concept ACK\n\nBuilt each commit and ran the unit tests, with a before and after of both cases against master."
  },
  {
   "t": "2026-09-08T18:27:14Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/util/mining.cpp",
   "commit": "6052be3135c35573017263509734476ed43eb975",
   "in_reply_to": null,
   "text": "This is an assignment rather than an adjustment, so the template's fees go with it. `MinerTestingSetup::Block` asks for its template with the mempool on, and with one transaction in it the coinbase reward comes out at 5000001000 on master and 5000000000 here, with the block still carrying that transaction.\n\nNothing hits it today, since the mempool is empty at every call. Would `.use_mempool = false` on that template call be simpler? It makes the site match `BuildChain`, so the overwrite is exact, and the three suites stay green with it."
  },
  {
   "t": "2026-09-08T18:27:14Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/util/mining.cpp",
   "commit": "6052be3135c35573017263509734476ed43eb975",
   "in_reply_to": null,
   "text": "Removing the target, subsidy or padding line here leaves `blockfilter_index_tests` and `baseindex_tests` green, so nothing on that side covers what the branch changes. The new assertions in `MinerTestingSetup::Block` catch the subsidy and the padding, and the target has nothing anywhere.\n\nThese two cover all four fields, if they are worth having. The target one calls the helper directly, which is the only way in, since on regtest the recalculated target always equals the template's and no chain can tell the line apart. The coinbase one passes here and fails on master, first on the script length and then at the halving block. Both compile in `blockfilter_index_tests.cpp`.\n\n```cpp\nBOOST_FIXTURE_TEST_CASE(rebuild_block_for_parent_target, RegTestingSetup)\n{\n    const auto& consensus{Params().GetConsensus()};\n    const auto genesis{Params().GenesisBlock()};\n    const auto parent_hash{genesis.GetHash()};\n    CBlockIndex parent{genesis};\n    parent.phashBlock = &parent_hash;\n    --parent.nBits; // A valid target, different from the one the template carries.\n\n    // Regtest allows min-difficulty blocks, and parent has no pprev, so\n    // GetNextWorkRequired returns the parent's target below the threshold and the\n    // pow limit above it.\n    const auto threshold{uint32_t(2 * consensus.nPowTargetSpacing)};\n    for (const auto delay : {1U, threshold, threshold + 1}) {\n        CBlock block{genesis};\n        RebuildBlockForParent(block, parent, parent.nTime + delay, consensus);\n        BOOST_CHECK_EQUAL(block.nBits, delay > threshold ? genesis.nBits : parent.nBits);\n    }\n}\n\nBOOST_FIXTURE_TEST_CASE(rebuild_block_for_parent_coinbase, TestChain100Setup)\n{\n    const auto& consensus{Params().GetConsensus()};\n    auto process = [&](const std::vector<std::shared_ptr<CBlock>>& chain) {\n        for (const auto& block : chain) {\n            BOOST_CHECK(m_node.chainman->ProcessNewBlock(block, /*force_processing=*/true, /*min_pow_checked=*/true, nullptr));\n        }\n    };\n\n    // A fork rooted below height 17 needs the dummy OP_0, since the BIP34\n    // height alone is one byte there (bad-cb-length).\n    {\n        const CBlockIndex* genesis{WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Genesis())};\n        std::vector<std::shared_ptr<CBlock>> fork;\n        BOOST_REQUIRE(BuildChain(m_node, genesis, CScript() << OP_TRUE, 3, fork));\n        BOOST_CHECK_EQUAL(fork.front()->vtx[0]->vin[0].scriptSig.size(), 2U);\n        process(fork);\n    }\n\n    // BuildChain only submits headers, so every template keeps being built on\n    // the active tip. A chain long enough to pass the halving would otherwise\n    // carry the tip's subsidy and fail to connect (bad-cb-amount).\n    {\n        const CBlockIndex* tip{WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Tip())};\n        const int start{tip->nHeight};\n        const int length{consensus.nSubsidyHalvingInterval + 10 - start};\n        BOOST_REQUIRE(length > 0);\n        std::vector<std::shared_ptr<CBlock>> chain;\n        BOOST_REQUIRE(BuildChain(m_node, tip, CScript() << OP_TRUE, length, chain));\n        for (int i = 0; i < length; ++i) {\n            BOOST_CHECK_EQUAL(chain.at(i)->vtx[0]->vout[0].nValue, GetBlockSubsidy(start + 1 + i, consensus));\n        }\n        process(chain);\n        BOOST_CHECK_EQUAL(WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Height()), start + length);\n    }\n}\n```"
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-28T00:17:34Z",
   "action": "labeled",
   "label": "Tests",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 6631,
 "text_tokens_estimate": 1657,
 "changed_paths": [
  "src/test/util/mining.cpp",
  "src/test/util/mining.h",
  "src/test/validation_block_tests.cpp"
 ],
 "files": [
  {
   "path": "src/test/util/mining.cpp",
   "add": 18,
   "del": 9
  },
  {
   "path": "src/test/util/mining.h",
   "add": 7,
   "del": 0
  },
  {
   "path": "src/test/validation_block_tests.cpp",
   "add": 7,
   "del": 6
  }
 ],
 "test_lines": 47,
 "git": {
  "head": "6052be3135c35573017263509734476ed43eb975",
  "head_matches_backup": true,
  "base": "ea7d459ac08b05521021e3d273625424e1b9b19e",
  "commits": [
   {
    "sha": "afcf26f0ef",
    "subject": "test: extract fork block reconstruction",
    "files": 2,
    "add": 18,
    "del": 9
   },
   {
    "sha": "4eee9d5275",
    "subject": "test: derive fork difficulty from parent",
    "files": 2,
    "add": 7,
    "del": 3
   },
   {
    "sha": "b82a725a6c",
    "subject": "test: derive fork subsidy from parent",
    "files": 2,
    "add": 9,
    "del": 4
   },
   {
    "sha": "6052be3135",
    "subject": "test: pad low-height fork coinbases",
    "files": 2,
    "add": 3,
    "del": 4
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "13d9776857432997",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}