test: harden arbitrary-parent block creation #36109

pull l0rinc wants to merge 4 commits into bitcoin:master from l0rinc:l0rinc/test-build-valid-fork-blocks changing 3 files +32 −15
  1. l0rinc commented at 12:17 AM on August 28, 2026: contributor

    Problem: While reviewing #35847, a few hardening opportunities came up in the test helpers that attach active-tip block templates to arbitrary parents. The 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. Existing callers already avoid these cases or handle them locally, so no current test fails.

    Fix: Share the parent-dependent reconstruction between BuildChain() and MinerTestingSetup::Block(). Use 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. Require the validation setup's completed block to retain the reconstructed locktime, script, and subsidy.

  2. test: extract fork block reconstruction
    `BuildChain` creates a block template for the active tip and then rewrites it for the requested parent.
    Move the existing parent-dependent field rewrites into a helper while passing the existing timestamp explicitly, without changing the generated fields or behavior.
    afcf26f0ef
  3. test: derive fork difficulty from parent
    `BuildChain` copies the work target from a block template for the active tip before attaching the block to its requested parent.
    Recalculate `nBits` from that parent and the rebuilt header so the target describes the resulting child.
    
    Current callers use regtest with retargeting disabled, so this prevents future invalid test cases rather than fixing an existing failure.
    4eee9d5275
  4. test: derive fork subsidy from parent
    `BuildChain` and `validation_block_tests::MinerTestingSetup::Block` attach active-tip templates to arbitrary parents, but retain the template subsidy.
    Derive the resulting height and subsidy in the shared reconstruction so both builders pay the correct coinbase reward.
    
    `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.
    
    Current callers do not cross a subsidy halving, so this prevents future invalid test cases rather than fixing an existing failure.
    b82a725a6c
  5. test: pad low-height fork coinbases
    `RebuildBlockForParent` replaces the template coinbase input script with the child height, which encodes to one byte at heights 1 through 16.
    Append 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.
    6052be3135
  6. DrahtBot added the label Tests on Aug 28, 2026
  7. DrahtBot commented at 12:17 AM on August 28, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/36109.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK jeanpablojp

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35675 (mining: add block template manager by ismaelsadeeq)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  8. jeanpablojp commented at 6:27 PM on September 8, 2026: contributor

    Concept ACK

    Built each commit and ran the unit tests, with a before and after of both cases against master.

  9. in src/test/util/mining.cpp:89 in 6052be3135
      84 | +        const int height{parent.nHeight + 1};
      85 | +        CMutableTransaction tx_coinbase{*block.vtx.at(0)};
      86 | +        tx_coinbase.nLockTime = static_cast<uint32_t>(parent.nHeight);
      87 | +        // Include a dummy OP_0 so low-height coinbase input scripts meet the minimum length
      88 | +        tx_coinbase.vin.at(0).scriptSig = CScript{} << height << OP_0;
      89 | +        tx_coinbase.vout.at(0).nValue = GetBlockSubsidy(height, params);
    


    jeanpablojp commented at 6:27 PM on September 8, 2026:

    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.

    Nothing 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.

  10. in src/test/util/mining.cpp:78 in 6052be3135
      74 | @@ -75,6 +75,23 @@ std::vector<std::shared_ptr<CBlock>> CreateBlockChain(size_t total_height, const
      75 |      return ret;
      76 |  }
      77 |  
      78 | +void RebuildBlockForParent(CBlock& block, const CBlockIndex& parent, uint32_t time, const Consensus::Params& params)
    


    jeanpablojp commented at 6:27 PM on September 8, 2026:

    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.

    These 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.

    BOOST_FIXTURE_TEST_CASE(rebuild_block_for_parent_target, RegTestingSetup)
    {
        const auto& consensus{Params().GetConsensus()};
        const auto genesis{Params().GenesisBlock()};
        const auto parent_hash{genesis.GetHash()};
        CBlockIndex parent{genesis};
        parent.phashBlock = &parent_hash;
        --parent.nBits; // A valid target, different from the one the template carries.
    
        // Regtest allows min-difficulty blocks, and parent has no pprev, so
        // GetNextWorkRequired returns the parent's target below the threshold and the
        // pow limit above it.
        const auto threshold{uint32_t(2 * consensus.nPowTargetSpacing)};
        for (const auto delay : {1U, threshold, threshold + 1}) {
            CBlock block{genesis};
            RebuildBlockForParent(block, parent, parent.nTime + delay, consensus);
            BOOST_CHECK_EQUAL(block.nBits, delay > threshold ? genesis.nBits : parent.nBits);
        }
    }
    
    BOOST_FIXTURE_TEST_CASE(rebuild_block_for_parent_coinbase, TestChain100Setup)
    {
        const auto& consensus{Params().GetConsensus()};
        auto process = [&](const std::vector<std::shared_ptr<CBlock>>& chain) {
            for (const auto& block : chain) {
                BOOST_CHECK(m_node.chainman->ProcessNewBlock(block, /*force_processing=*/true, /*min_pow_checked=*/true, nullptr));
            }
        };
    
        // A fork rooted below height 17 needs the dummy OP_0, since the BIP34
        // height alone is one byte there (bad-cb-length).
        {
            const CBlockIndex* genesis{WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Genesis())};
            std::vector<std::shared_ptr<CBlock>> fork;
            BOOST_REQUIRE(BuildChain(m_node, genesis, CScript() << OP_TRUE, 3, fork));
            BOOST_CHECK_EQUAL(fork.front()->vtx[0]->vin[0].scriptSig.size(), 2U);
            process(fork);
        }
    
        // BuildChain only submits headers, so every template keeps being built on
        // the active tip. A chain long enough to pass the halving would otherwise
        // carry the tip's subsidy and fail to connect (bad-cb-amount).
        {
            const CBlockIndex* tip{WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Tip())};
            const int start{tip->nHeight};
            const int length{consensus.nSubsidyHalvingInterval + 10 - start};
            BOOST_REQUIRE(length > 0);
            std::vector<std::shared_ptr<CBlock>> chain;
            BOOST_REQUIRE(BuildChain(m_node, tip, CScript() << OP_TRUE, length, chain));
            for (int i = 0; i < length; ++i) {
                BOOST_CHECK_EQUAL(chain.at(i)->vtx[0]->vout[0].nValue, GetBlockSubsidy(start + 1 + i, consensus));
            }
            process(chain);
            BOOST_CHECK_EQUAL(WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Height()), start + length);
        }
    }
    

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-09 07:56 UTC