Skip to content

policy: v31.1 overlay (3.9M WU, 1-sat dust, pinned datacarrier) - #4

Closed
cre8tor-devops wants to merge 1 commit into
bitcoindogmode:31.1-dogmodefrom
cre8tor-devops:dogmode/policy-overlay-31.1
Closed

cre8tor-devops wants to merge 1 commit into
bitcoindogmode:31.1-dogmodefrom
cre8tor-devops:dogmode/policy-overlay-31.1

Conversation

@cre8tor-devops

Copy link
Copy Markdown

Summary

This is a first real code PR against 31.1-dogmode, not an RFC. It implements the announced relay-policy overlay as a thin delta on Bitcoin Core v31.1 and records the side effects that a one-constant patch would have hidden.

  • Standard tx weight 400k → 3,900,000 WU, package weight 3,904,000 WU (same 4k WU headroom Core uses), cluster default 976 kvB so the static_assert chain still holds.
  • OP_RETURN stays 100,000 vB. MAX_OP_RETURN_RELAY is no longer MAX_STANDARD_TX_WEIGHT / 4, so the weight change does not silently lift datacarrier to 975k.
  • Default -dustrelayfee 0 (1-sat floor). The flag still works; -dustrelayfee=0.00003000 restores Core.
  • User-agent /DOGMode:31.1.0/, CLIENT_NAME "Bitcoin DOG Mode".
  • -maxmempool floor is now 40 MB (Flatten() is cluster_vbytes * 40). -blocksonly implies 40 MB instead of 5. Tests that need a 5 MB mempool also pin -limitclustersize=101.

Not in this PR: preferential peering. That needs a service-bit decision (#2) and should stay a separate commit.

Spec: doc/policy/dogmode.md. Release notes: doc/release-notes-dogmode.md.

This follows the measurements in #1 (thanks @tialkan). Where that RFC left behaviour questions open, this PR picks:

Question Choice
OP_RETURN coupling Pin at 100k; say so in the spec
Dust Default change, not a new hardcoded rule
package-too-large vs count Keep both limits; tests now trip weight first with a larger placeholder
Peering Follow-up

Test plan

  • cmake -B build && cmake --build build -j$(nproc)
  • ctest --test-dir build -R 'transaction_tests|txpackage_tests|coinselector_tests|txvalidation_tests'
  • build/test/functional/test_runner.py mempool_accept mempool_dust mempool_limit mempool_datacarrier rpc_packages
  • bitcoin-cli -regtest getnetworkinfo shows /DOGMode:
  • bitcoind -maxmempool=4 refuses to start with "at least 40 MB"
  • bitcoind -dustrelayfee=0.00003000 restores the 546-sat P2PKH floor

I could not run the C++ suite in this environment (no cmake/boost). Reviewers with a v31.1 build env should treat the test plan as required, not optional.

Made with Cursor

Raise standard tx/package/cluster limits together so the tree still compiles, pin OP_RETURN at 100k so datacarrier does not ride along, default -dustrelayfee to 0, and identify the client as /DOGMode/. Document the 40 MB -maxmempool floor and leave preferential peering for a follow-up.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cre8tor-devops

Copy link
Copy Markdown
Author

Closing this in favor of #3.

That PR already implements the three announced changes, has been independently built and tested, and has real review on the constants, dust semantics, peering, and the -maxmempool floor. Stacking a second overlay that only covers two of the three changes, without a local test run, is not useful.

If maintainers want a follow-up after #3, the remaining product choices I would still argue for are:

  • pin MAX_OP_RETURN_RELAY at 100k unless a 975k datacarrier default is an explicit goal
  • make 1-sat dust a -dustrelayfee default rather than a hard cap in GetDustThreshold(), so operators can still choose a stricter floor
  • keep Flatten()'s 40x multiplier and document the 40 MB -maxmempool floor, instead of cutting the DoS margin to preserve -maxmempool=5

Happy to review or patch those on top of #3 if wanted. Not going to compete with a tested three-commit series.

@tialkan

tialkan commented Aug 28, 2026

Copy link
Copy Markdown

Ran the test plan, since the description asks for it. macOS arm64, clang 21, -DENABLE_IPC=OFF, clean worktree.

Build: clean. No errors.

The three manual checks all pass exactly as described:

  • bitcoind -maxmempool=4 refuses to start: -maxmempool must be at least 40 MB
  • getnetworkinfo shows "subversion": "/DOGMode:31.1.0/"
  • -dustrelayfee=0.00003000 restores the old thresholds: 546 P2PKH, 540 P2SH-P2WPKH, 294 P2WPKH, 330 P2TR

Unit tests: 13 failures across 3 cases.

txpackage_tests/package_validation_tests, coinselector_tests/check_max_selection_weight, orphanage_tests/DoS_mapOrphans.

The giant-tx loop overshoots into consensus territory

package_validation_tests now fails with bad-txns-oversize != tx-size. That is the consensus block weight check firing, not the policy check the test is written to exercise.

The new loop grows the placeholder in steps of 500:

size_t giant_n{2000};
while (GetTransactionWeight(*giant_ptx) <= MAX_STANDARD_TX_WEIGHT) {
    giant_n += 500;
    giant_ptx = create_placeholder_tx(giant_n, giant_n);
}

Each create_placeholder_tx input/output pair is 66-byte scripts with no witness, so one step of 500 adds 364,000 WU. The gap between MAX_STANDARD_TX_WEIGHT (3,900,000) and MAX_BLOCK_WEIGHT (4,000,000) is 100,000 WU. So the first iteration past the policy ceiling lands at n=5500, weight 4,004,056, which is over the consensus limit:

n weight
5000 3,640,056
5500 4,004,056

At these constants the loop can never produce a transaction that is over the policy ceiling but under the consensus one, so tx-size is unreachable. A step of 100 rather than 500 would land inside the window, or the count could be computed directly.

coinselector: scenario 3 was rewritten, scenario 1 still fails

The failure is at line 1340, in scenario 1, not the scenario 3 that was rewritten. It is the has_coin(result->GetInputSet(), CAmount(50 * COIN)) assertion: with a 9.75x larger weight ceiling, the 1515 small coins now fit, so the selector no longer needs the 50 BTC coin. Scenarios 1 and 2 are built around coin counts chosen to overflow a 400,000 WU ceiling and do not test what they were written to test at 3,900,000.

orphanage: not touched, and there is more to it than the test

orphanage_tests is unmodified in this PR, so DoS_mapOrphans still fails on the hardcoded tx.vin.resize(2777), which used to be over the ceiling and now is not. That part is mechanical.

The part I would not treat as mechanical is what the ceiling is doing there. TxOrphanageImpl::AddTx uses it directly:

// Ignore transactions above max standard size to avoid a send-big-orphans memory exhaustion attack.
TxOrphanage::Usage sz = GetTransactionWeight(*tx);
if (sz > MAX_STANDARD_TX_WEIGHT) { ... return false; }

and the per-peer orphan memory reservation next to it is unchanged:

static constexpr int64_t DEFAULT_RESERVED_ORPHAN_WEIGHT_PER_PEER{404'000};

404,000 is not an arbitrary number: it is MAX_PACKAGE_WEIGHT in stock Core, sized so a single maximum-size orphan just fits inside one peer's whole reservation.

single max orphan / per-peer reservation
stock v31.1 0.99x
this PR 9.65x

MaxGlobalUsage() is m_reserved_usage_per_peer * peers, so the global bound moves with peer count while a single orphan is now nearly ten times one peer's share. Neither this PR nor #3 changes DEFAULT_RESERVED_ORPHAN_WEIGHT_PER_PEER. Given the comment above it says explicitly what the bound is for, this looks like it wants a decision rather than a test update.

Functional tests: mempool_dust fails, mempool_accept hangs.

mempool_dust got the constant updated (DUST_RELAY_TX_FEE = 0) but not the logic. test_dustrelay ends by restarting with extra_args=[] and asserting the mempool is then empty, which assumes the default policy is stricter than whatever the test set. With the default now at 0, nothing is stricter than the default, so that assertion cannot hold:

AssertionError: not(['9579...', 'c7c4...'] == [])

mempool_accept did not finish. It sat for over 20 minutes on "A transaction padded to the max standardness tx size (not via OP_RETURN; datacarrier stays 100k)" before I stopped it. Padding to 3.9M WU through the Python framework is roughly ten times the work it used to be, and whatever CI ends up running this will need a timeout that accounts for it.


For what it is worth, the direction here reads better than #3 to me on all three of the open questions: pinning MAX_OP_RETURN_RELAY at 100,000 rather than letting it ride on the weight constant, the dust default rather than a hardcoded cap, and keeping the 40x mempool multiplier. I measured the last of those separately and a 5x multiplier at the floor leaves one maximum-size transaction at about 19% of the mempool, against 2% today, which makes the minimum feerate move in very coarse steps. 40 MB avoids that entirely.

Measurements and harness: https://github.com/tialkan/bitcoin-dust-utxo-lab

@tialkan

tialkan commented Aug 28, 2026

Copy link
Copy Markdown

Correction to my comment above, on the orphan pool point. I measured it and I had it wrong.

I argued that because TxOrphanageImpl::AddTx bounds a single orphan by MAX_STANDARD_TX_WEIGHT while DEFAULT_RESERVED_ORPHAN_WEIGHT_PER_PEER stays at 404,000, one maximum-size orphan goes from 0.99x to 9.65x a peer's whole reservation, and that this wanted a decision.

The ratio is real. The consequence I drew from it is not.

Largest orphan actually retained, sending a child whose parent was never submitted and counting getorphantxs, node restarted between probes:

Target vsize Weight stock v31.1 this PR
99,000 395,999 accepted accepted
100,000 399,999 accepted accepted
100,500 401,999 rejected rejected
101,000 403,999 rejected rejected
300,000 1,199,999 rejected rejected
974,000 3,895,999 rejected rejected

Identical on both, probe for probe. The effective ceiling stays at about 400,000 WU because the per-peer reservation binds first: LimitOrphans trims anything that takes a peer past its allowance, so a 3.9M WU orphan is dropped on arrival whatever AddTx permits. The memory a single peer can pin does not change, and the send-big-orphans protection the comment describes still holds. A different constant is providing it, that is all.

So two things I said should be withdrawn. The orphan pool does not need a decision here, and orphanage_tests/DoS_mapOrphans is a mechanical fix after all: it hardcodes tx.vin.resize(2777), sized against a 400,000 WU ceiling, and deriving that count from the constant is enough. I listed it as a behaviour question in my #1 as well and have corrected that too.

What is left is minor and not a DoS matter: the AddTx check against MAX_STANDARD_TX_WEIGHT is no longer reachable for the case it was written for, since anything large enough is trimmed before it can bind. The comment above it now describes a guarantee that a different line provides. Worth a note if you touch that file, not worth a change on its own.

The other four points in my earlier comment stand as measured. Harness for this one is scripts/exp10_orphanage_bound.py in the lab repo; it is a functional test rather than a standalone script, since orphans only arrive over P2P.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants