Limit “Bulk Dust” with a default filter or consensus.. #33737

issue DoctorBuzz1 openend this issue on October 29, 2025
  1. DoctorBuzz1 commented at 8:00 pm on October 29, 2025: none

    I’m exploring a potential default filter or consensus-level rule (since a large number of people believe that default filters don’t work) to discourage UTXO-bloat patterns without touching Script, witness data, or the block size limit.

    The idea is to target “bulk dust” transactions — those that create large numbers of extremely small outputs — which are the main cause of long-term UTXO set growth.

    These types of “bulk dust” transactions have been the No. 1 reason cited for wanting to expand the default OP_RETURN limit… and removing that limit obviously influenced BIP 444. So it appears to me that there is overwhelming majority support for limiting these types of “bulk dust” transactions, as they do present a legitimate concern for node runners.

    Concept

    Flag a transaction as “bulk dust” if:

    • It has >=100 outputs each below a dynamically defined TinyTx threshold, and
    • Those tiny outputs make up >=60% of all outputs in the transaction.

    When flagged, it would be considered nonstandard (relay policy) or invalid (if soft-forked into consensus).

    TinyTx threshold (dynamic halving schedule)

    I originally considered a constant definition of what was a “tiny” Tx to be 1,000 sats… but some might still just use 1,001 sats, right? Plus there very likely will be a time where there is a valid use-case of >100 outputs under 1,000 sats.

    Rather than fixing the “tiny” threshold to a constant like 1,000 sats, the rule defines it as a decreasing function of block height, starting high and gradually tightening over time.

    • Starts at 4096 sats when activated (target ~2028).
    • Halves every 210,000 blocks (~4 years).
    • Never falls below 1 sat (hard floor).

    Year —- Block Height – TinyTx Threshold 2028 — ~activation —- 4096 sats 2032 — ~1,260,000 —- 2048 sats 2036 — ~1,470,000 —- 1024 sats 2040 — ~1,680,000 —- 512 sats … – every 210,000 blocks – … until 1 sat floor

    This gradual halving ensures the definition of “tiny” stays relevant as Bitcoin’s value rises. For example, if 1 sat = $1 someday, having 100 outputs worth <1,000 sats each would no longer represent spam — but rather normal payments. By then, the TinyTx limit would already have adjusted down automatically.

    Patterns this would limit

    • Fake pubkeys or scripts used to embed data via many UTXOs
    • Bitcoin STAMPS / UTXO-art spreading payloads across thousands of dust outputs
    • BRC-20 batch mints with 100s of “tiny” sat fan-outs
    • Some Ordinal or state inscription schemes that distribute data across many tiny outputs
    • Dust bombing (UTXO tracking or chain spam)
    • Mass micro-airdrops below the “tiny” sat range

    These use cases rely on cheap, numerous outputs — making them several times more costly under this rule.

    Non-goals / unaffected

    • Normal user transactions, LN channel opens, and multisig spends
    • Batched exchange payouts (they typically have > 40% large-value outputs)
    • Single/few-output inscriptions using witness data (not affected)
    • Any legitimate pattern where most outputs are above the threshold

    Why a ratio and a count?

    Requiring both (tiny_count >= 100) and (tiny_ratio >= 60%) helps avoid false positives, such as legitimate custodial payouts or consolidation transactions with mixed values. It specifically filters transactions that are mostly dust, rather than merely containing some.

    Inquiry

    • Are there credible, non-spam use cases that truly require >=100 sub-4k-sat outputs (or equivalent at later eras) and a >=60% tiny ratio?
    • Could this affect fee market behavior or any privacy tools in unintended ways?
    • Any concern with the 100 tiny_count limit or 60% tiny_ratio?
    • Any other unintended consequences?
    • Any objections in general?? What are they?

    Intent

    This proposal doesn’t censor any monetary transaction or prevent inscriptions; it simply prices storage according to resource cost. It keeps the chain “light and nimble” for everyday payments while allowing future flexibility — because the TinyTx definition decreases automatically in line with halvings and Bitcoin’s long-term value growth.

    CODE SKETCHES (with minimal syntax highlighting here: https://pastebin.com/9qdQCH83)

    RELAY POLICY FILTER sketch —

     0// Place in src/policy/policy.cpp, and call from within IsStandardTx() before returning:
     1//     if (IsBulkDust(tx, reason))
     2//         return false;   // reject as nonstandard
     3// ==========================================================================================================
     4
     5bool IsBulkDust(const CTransaction& tx, std::string& reason)
     6{
     7    static constexpr int     MAX_TINY_OUTPUTS        = 100;     // >=100 tiny outputs triggers ratio check
     8    static constexpr double  TINY_RATIO_THRESHOLD    = 0.6;     // >=60% of all outputs tiny  reject
     9    static constexpr CAmount BASE_TINY_THRESHOLD     = 4096;    // starting tiny threshold (sats)
    10    static constexpr int64_t FIRST_TINY_HALVING_H    = 1260000; // first halving of tiny threshold
    11    static constexpr int64_t HALVING_INTERVAL        = 210000;  // blocks per subsequent halving
    12    static constexpr CAmount MIN_TINY_FLOOR          = 1;       // never below 1 sat
    13
    14    const int total = tx.vout.size();
    15    if (total == 0) return false;
    16
    17    int currentHeight = chainActive.Tip() ? chainActive.Tip()->nHeight : 0;
    18
    19    // Era index for TinyTx threshold, anchored at FIRST_TINY_HALVING_H (not subsidy eras)
    20    int era = 0;
    21    if (currentHeight >= FIRST_TINY_HALVING_H) {
    22        era = 1 + static_cast<int>((currentHeight - FIRST_TINY_HALVING_H) / HALVING_INTERVAL);
    23    }
    24
    25    CAmount tinyThresh = BASE_TINY_THRESHOLD >> era;      // halve per era
    26    if (tinyThresh < MIN_TINY_FLOOR) tinyThresh = MIN_TINY_FLOOR;
    27
    28    int tiny = 0;
    29    for (const auto& out : tx.vout) {
    30        if (out.nValue < tinyThresh) ++tiny;
    31    }
    32
    33    if (tiny >= MAX_TINY_OUTPUTS && (static_cast<double>(tiny) / total) >= TINY_RATIO_THRESHOLD) {
    34        reason = strprintf("too-many-tiny-outputs(%d of %d, %.2f%%, tiny<%d)",
    35                           tiny, total, 100.0 * tiny / total, tinyThresh);
    36        return true; // flag as bulk dust (nonstandard)
    37    }
    38    return false;
    39}
    

    CONSENSUS (soft-fork, hybrid activation) sketch —

     0// Helpers in src/consensus/tx_check.cpp; activation/enforcement in src/validation.cpp
     1// Also define deployment in: src/consensus/params.h, src/chainparams.cpp, src/versionbits.*
     2// ==========================================================================================================
     3
     4// -----------------------------------------------------------------------
     5// --- In src/consensus/tx_check.cpp (helper only; no params needed) ---
     6// -----------------------------------------------------------------------
     7
     8static constexpr CAmount BASE_TINY_THRESHOLD     = 4096;
     9static constexpr int64_t FIRST_TINY_HALVING_H    = 1260000;
    10static constexpr int64_t HALVING_INTERVAL        = 210000;
    11static constexpr int     MAX_TINY_OUTPUTS        = 100;
    12static constexpr double  TINY_RATIO_THRESHOLD    = 0.6;
    13static constexpr CAmount MIN_TINY_FLOOR          = 1;
    14
    15bool IsBulkDust(const CTransaction& tx, int currentHeight) // expose via tx_check.h if needed
    16{
    17    const int total = tx.vout.size();
    18    if (total == 0) return false;
    19
    20    int era = 0;
    21    if (currentHeight >= FIRST_TINY_HALVING_H) {
    22        era = 1 + static_cast<int>((currentHeight - FIRST_TINY_HALVING_H) / HALVING_INTERVAL);
    23    }
    24
    25    CAmount tinyThresh = BASE_TINY_THRESHOLD >> era;
    26    if (tinyThresh < MIN_TINY_FLOOR) tinyThresh = MIN_TINY_FLOOR;
    27
    28    int tiny = 0;
    29    for (const auto& out : tx.vout) {
    30        if (out.nValue < tinyThresh) ++tiny;
    31    }
    32
    33    if (tiny >= MAX_TINY_OUTPUTS && (static_cast<double>(tiny) / total) >= TINY_RATIO_THRESHOLD)
    34        return true;
    35
    36    return false;
    37}
    38
    39
    40// -----------------------------------------------------------------------
    41// --- In src/validation.cpp (enforcement with hybrid activation) ---
    42// -----------------------------------------------------------------------
    43
    44#include <consensus/tx_check.h>
    45#include <versionbits.h>
    46
    47const Consensus::Params& params = chainparams.GetConsensus();
    48int currentHeight = chainActive.Tip() ? chainActive.Tip()->nHeight : 0;
    49
    50const bool bulk_dust_active =
    51    DeploymentActiveAtTip(params, Consensus::DEPLOYMENT_BULK_DUST_LIMIT) ||
    52    (currentHeight >= params.BulkDustActivationHeight);
    53
    54if (bulk_dust_active) {
    55    if (IsBulkDust(tx, currentHeight)) {
    56        return state.Invalid(TxValidationResult::TX_CONSENSUS, "too-many-tiny-outputs");
    57    }
    58}
    59
    60
    61// -----------------------------------------------------------------------
    62// --- In src/consensus/params.h ---
    63// -----------------------------------------------------------------------
    64
    65enum DeploymentPos {
    66    // ...
    67    DEPLOYMENT_BULK_DUST_LIMIT,
    68    MAX_VERSION_BITS_DEPLOYMENTS
    69};
    70
    71struct Params {
    72    // ...
    73    int BulkDustActivationHeight; // height flag-day fallback
    74};
    75
    76
    77// -----------------------------------------------------------------------
    78// --- In src/chainparams.cpp (per-network values; examples only) ---
    79// -----------------------------------------------------------------------
    80
    81consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].bit = 12;
    82consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nStartTime = 1767225600;  // 2026-01-01 UTC
    83consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nTimeout   = 1838160000;  // 2028-04-01 UTC
    84consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].min_activation_height = 969696;
    85
    86consensus.BulkDustActivationHeight = 1021021; // flag-day fallback
    
  2. Frenchanfry commented at 11:51 pm on October 29, 2025: none

    I love this idea to keep spammers away; it also is fundamental when bitcoins value isn’t compared relatively to the $. to keep the network clean and efficient.

    1 satoshi = 1 satoshi.

  3. pinheadmz commented at 11:53 pm on October 29, 2025: member
    This should be posted on the bitcoin-dev mailing list, the Delving Bitcoin forum or some other platform where broad, protocol-level concepts are discussed. Conceptual questions and most usage questions can be posted on Stack Exchange. The Bitcoin Core issue tracker is reserved for discussion about this specific software project only, its implementation and usage.
  4. pinheadmz closed this on Oct 29, 2025


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: 2025-11-02 18:12 UTC

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