policy: use 64-bit accumulator for package total weight #35473

pull digin1 wants to merge 1 commits into bitcoin:master from digin1:policy-package-weight-64bit changing 2 files +40 −1
  1. digin1 commented at 3:33 PM on June 6, 2026: none

    IsWellFormedPackage() enforces MAX_PACKAGE_WEIGHT by summing each transaction's weight:

    const int64_t total_weight = std::accumulate(txns.cbegin(), txns.cend(), 0,
                               [](int64_t sum, const auto& tx) { return sum + GetTransactionWeight(*tx); });
    if (package_count > 1 && total_weight > MAX_PACKAGE_WEIGHT) { ... "package-too-large" }
    

    std::accumulate's accumulator type is the type of the init value. The init here is the literal 0, so the accumulator is an int: the 64-bit lambda result is narrowed back to 32 bits on every iteration. A package whose true combined weight exceeds INT_MAX therefore wraps to a small or negative value, and the total_weight > MAX_PACKAGE_WEIGHT check is silently bypassed.

    This is the only place MAX_PACKAGE_WEIGHT is enforced. The two sibling weight/fee accumulations in the package-acceptance path already use a 64-bit init (int64_t{0} / CAmount{0}); this call site is the inconsistent one.

    Impact

    Limited and policy-only — not a consensus issue:

    • Oversized transactions are still rejected individually by the per-transaction weight/standardness checks that run after IsWellFormedPackage(), so this does not let over-weight transactions into the mempool.
    • The effect is that the package-level early rejection (package-too-large) fails to fire for a package whose combined weight crosses INT_MAX, so the node does more work before rejecting. Such a package is reachable e.g. via the submitpackage RPC with sufficiently large transactions.

    Still, the guard should do what it says, and relying on 32-bit truncation of a weight sum is fragile.

    Fix

    One line — initialise the accumulator with int64_t{0}.

    Test

    Adds package_weight_overflow_tests to txpackage_tests.cpp. Because triggering the wrap requires a combined weight above INT_MAX and MAX_PACKAGE_COUNT is 25, each transaction must be large. To avoid allocating 25 separate large transactions, the test reuses one ~27 MB transaction 25 times — the weight check runs before the duplicate-txid check, so the accumulator still sees 25 × weight. The test asserts the rejection reason is package-too-large; on the unfixed code the wrapped total bypasses the weight check and the reason is instead package-contains-duplicates. Happy to drop or slim the test if the maintainers prefer.

    How to reproduce the mechanism

    The truncation can be observed in isolation with a minimal program that mirrors the exact std::accumulate expression: 25 weights of ~90,000,000 sum to 2,250,000,000 (> INT_MAX); the int-accumulator version returns -2,044,967,296 (guard bypassed) while an int64_t accumulator returns the correct 2,250,000,000 (guard fires).

  2. policy: use 64-bit accumulator for package total weight
    IsWellFormedPackage() sums each transaction's weight to enforce
    MAX_PACKAGE_WEIGHT, but the std::accumulate() initial value is a plain
    `0`, which makes the accumulator an `int`. The 64-bit lambda result is
    therefore narrowed back to 32 bits on every iteration, so a package
    whose combined weight exceeds INT_MAX wraps to a small or negative
    value and slips past the "package-too-large" check.
    
    Initialise the accumulator with int64_t{0} so the running total is kept
    in 64 bits, consistent with the other weight/fee accumulations in the
    package validation path.
    
    Add a regression test that reuses a single large transaction
    MAX_PACKAGE_COUNT times so the summed weight crosses INT_MAX while
    keeping the test's footprint to one transaction. With the bug present
    the wrapped total bypasses the weight check and the package is instead
    rejected as "package-contains-duplicates"; with the fix it is correctly
    rejected as "package-too-large".
    3b284da1ae
  3. digin1 requested review from Copilot on Jun 6, 2026
  4. DrahtBot added the label TX fees and policy on Jun 6, 2026
  5. DrahtBot commented at 3:33 PM on June 6, 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/35473.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  6. Copilot commented at 3:34 PM on June 6, 2026: none

    Pull request overview

    [!NOTE] Copilot was unable to run its full agentic suite in this review.

    Fixes an integer-truncation issue in IsWellFormedPackage’s package-weight calculation and adds a regression test to ensure oversized packages are rejected for the correct reason.

    Changes:

    • Use a 64-bit initial value in std::accumulate to prevent narrowing the running sum to int.
    • Add a regression test that exercises the overflow/truncation scenario and asserts the correct reject reason.

    Reviewed changes

    Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

    File Description
    src/policy/packages.cpp Prevents truncation by making the std::accumulate initial value int64_t{0}.
    src/test/txpackage_tests.cpp Adds a regression test for the weight-accumulator truncation case and includes <limits> for bounds checks.

    💡 <a href="/bitcoin/bitcoin/new/master?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.

  7. in src/test/txpackage_tests.cpp:231 in 3b284da1ae
     226 | +    // Preconditions for this regression to be meaningful: a single tx is under
     227 | +    // INT_MAX, but MAX_PACKAGE_COUNT copies exceed it (and stay below 2^32, so a
     228 | +    // 32-bit accumulator wraps to a value <= MAX_PACKAGE_WEIGHT).
     229 | +    BOOST_REQUIRE(tx_weight <= std::numeric_limits<int32_t>::max());
     230 | +    BOOST_REQUIRE(total_true_weight > std::numeric_limits<int32_t>::max());
     231 | +    BOOST_REQUIRE(total_true_weight < (int64_t{1} << 32));
    


    Copilot commented at 3:34 PM on June 6, 2026:

    The regression conditions are tied to int32_t, but the original bug is driven by the type deduced from the literal 0 (i.e., int), which is not guaranteed to be 32-bit on all platforms/toolchains. To make the test robust and self-documenting, base these preconditions on std::numeric_limits<int>::max() (and/or gate the test with a sizeof(int) == 4 requirement), so the test only asserts the overflow/truncation behavior when the platform characteristics match the bug scenario.

  8. in src/test/txpackage_tests.cpp:223 in 3b284da1ae
     218 | +{
     219 | +    // ~27 MB tx => weight ~109M; 25 copies => true total ~2.73e9, which exceeds
     220 | +    // INT_MAX (~2.147e9) and so overflows a 32-bit accumulator, yet stays a single
     221 | +    // ~27 MB allocation rather than 25 separate large transactions.
     222 | +    CTransactionRef big_ptx = create_placeholder_tx(/*num_inputs=*/150'000, /*num_outputs=*/150'000);
     223 | +    const int64_t tx_weight = GetTransactionWeight(*big_ptx);
    


    Copilot commented at 3:34 PM on June 6, 2026:

    Constructing a transaction with 150k inputs and 150k outputs is likely to be significantly heavier than “~27 MB allocation” due to per-input/output object overhead, which can slow CI and increase peak memory usage (especially under sanitizers). Consider generating the required weight using a much smaller number of inputs/outputs (e.g., 1 in / 1 out) and inflating the serialized size via large scripts or data fields, so the test still reaches the >INT_MAX threshold while minimizing vector/object overhead and runtime.

  9. l0rinc commented at 3:55 PM on June 6, 2026: contributor

    Since when do we have copilot auto-review?

    <img width="1536" height="1024" alt="image" src="https://github.com/user-attachments/assets/3f4d6faa-5646-45b5-a2de-467f90d4b481" />

  10. pinheadmz commented at 4:12 PM on June 6, 2026: member

    @digin1 are you able to explain this code change in your own words? How you reviewed the LLM output? How you discovered the issue in the first place? How this would affect normal users?

    We require PR authors to provide the first "human pass" before consuming our very limited resources. Otherwise we'll have to close this PR

  11. digin1 commented at 4:23 PM on June 6, 2026: none

    Full transparency, since you asked directly:

    This came out of an unattended, automated investigation of the repo — an LLM-driven audit pass I was running to look for a specific class of mistake (integer-width / accumulator bugs). It flagged this line and I'm passing it along rather than sitting on it. I want to be upfront that it was machine-found, not a human-first review, so please weigh it accordingly.

    The finding itself is straightforward and checks out:

    • IsWellFormedPackage() sums each tx's weight with std::accumulate(txns.cbegin(), txns.cend(), 0, ...). accumulate's accumulator takes the type of the seed, and 0 is an int — so the running total is narrowed back to 32 bits every iteration even though the lambda returns int64_t. A package whose combined weight crosses INT_MAX wraps small/negative and slips past the > MAX_PACKAGE_WEIGHT check. Seeding with int64_t{0} keeps it 64-bit.
    • Impact looks low: not consensus, and over-weight txs are still rejected individually afterward, so nothing improper reaches the mempool — the only effect is the package-level early rejection failing to fire (reachable via submitpackage). Robustness, not security.
    • As far as I can tell it has never been reported: the int seed has been on master since #28471 (Sept 2023, when this check moved from vsize to weight) and survived the later refactors of this function — probably why it went unnoticed, since the lambda's int64_t sum makes the accumulator look 64-bit at a glance.

    No attachment to the PR — flagging it in case it's useful, and I'm completely fine closing it given it's machine-found and low-impact. If it's worth fixing, the one-liner stands on its own and you can take it from there however you prefer.

  12. sedited commented at 4:40 PM on June 6, 2026: contributor

    Closing this. I don't think the regression test makes sense at all the way it is right now, and I don't think it will be fun to prompt the author's llm to get it into better shape. If somebody wants to pick up this one line improvement, that seems fine as a tiny defense in depth change.

  13. sedited closed this on Jun 6, 2026

  14. digin1 deleted the branch on Jun 6, 2026

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-06-13 21:30 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me