public inbox for bitcoindev@googlegroups.com
 help / color / mirror / Atom feed
* [bitcoindev] By: Doctorbuzz1 {GitHub} Limit "Bulk Dust" with a default filter or consensus.
@ 2025-10-30  0:03 Frenchanfry
  2025-10-30  1:02 ` [bitcoindev] " Doctor Buzz
  0 siblings, 1 reply; 3+ messages in thread
From: Frenchanfry @ 2025-10-30  0:03 UTC (permalink / raw)
  To: Bitcoin Development Mailing List


[-- Attachment #1.1: Type: text/plain, Size: 10004 bytes --]

A proposal on GitHub I found Highly interesting and a better improvement, 
dealing with spammers/congestion.

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 —
// Place in src/policy/policy.cpp, and call from within IsStandardTx() 
before returning: // if (IsBulkDust(tx, reason)) // return false; // reject 
as nonstandard // 
========================================================================================================== 
bool IsBulkDust(const CTransaction& tx, std::string& reason) { static 
constexpr int MAX_TINY_OUTPUTS = 100; // >=100 tiny outputs triggers ratio 
check static constexpr double TINY_RATIO_THRESHOLD = 0.6; // >=60% of all 
outputs tiny → reject static constexpr CAmount BASE_TINY_THRESHOLD = 4096; 
// starting tiny threshold (sats) static constexpr int64_t 
FIRST_TINY_HALVING_H = 1260000; // first halving of tiny threshold static 
constexpr int64_t HALVING_INTERVAL = 210000; // blocks per subsequent 
halving static constexpr CAmount MIN_TINY_FLOOR = 1; // never below 1 sat 
const int total = tx.vout.size(); if (total == 0) return false; int 
currentHeight = chainActive.Tip() ? chainActive.Tip()->nHeight : 0; // Era 
index for TinyTx threshold, anchored at FIRST_TINY_HALVING_H (not subsidy 
eras) int era = 0; if (currentHeight >= FIRST_TINY_HALVING_H) { era = 1 + 
static_cast<int>((currentHeight - FIRST_TINY_HALVING_H) / 
HALVING_INTERVAL); } CAmount tinyThresh = BASE_TINY_THRESHOLD >> era; // 
halve per era if (tinyThresh < MIN_TINY_FLOOR) tinyThresh = MIN_TINY_FLOOR; 
int tiny = 0; for (const auto& out : tx.vout) { if (out.nValue < 
tinyThresh) ++tiny; } if (tiny >= MAX_TINY_OUTPUTS && 
(static_cast<double>(tiny) / total) >= TINY_RATIO_THRESHOLD) { reason = 
strprintf("too-many-tiny-outputs(%d of %d, %.2f%%, tiny<%d)", tiny, total, 
100.0 * tiny / total, tinyThresh); return true; // flag as bulk dust 
(nonstandard) } return false; } 

CONSENSUS (soft-fork, hybrid activation) sketch —
// Helpers in src/consensus/tx_check.cpp; activation/enforcement in 
src/validation.cpp // Also define deployment in: src/consensus/params.h, 
src/chainparams.cpp, src/versionbits.* // 
========================================================================================================== 
// ----------------------------------------------------------------------- 
// --- In src/consensus/tx_check.cpp (helper only; no params needed) --- // 
----------------------------------------------------------------------- 
static constexpr CAmount BASE_TINY_THRESHOLD = 4096; static constexpr 
int64_t FIRST_TINY_HALVING_H = 1260000; static constexpr int64_t 
HALVING_INTERVAL = 210000; static constexpr int MAX_TINY_OUTPUTS = 100; 
static constexpr double TINY_RATIO_THRESHOLD = 0.6; static constexpr 
CAmount MIN_TINY_FLOOR = 1; bool IsBulkDust(const CTransaction& tx, int 
currentHeight) // expose via tx_check.h if needed { const int total = 
tx.vout.size(); if (total == 0) return false; int era = 0; if 
(currentHeight >= FIRST_TINY_HALVING_H) { era = 1 + 
static_cast<int>((currentHeight - FIRST_TINY_HALVING_H) / 
HALVING_INTERVAL); } CAmount tinyThresh = BASE_TINY_THRESHOLD >> era; if 
(tinyThresh < MIN_TINY_FLOOR) tinyThresh = MIN_TINY_FLOOR; int tiny = 0; 
for (const auto& out : tx.vout) { if (out.nValue < tinyThresh) ++tiny; } if 
(tiny >= MAX_TINY_OUTPUTS && (static_cast<double>(tiny) / total) >= 
TINY_RATIO_THRESHOLD) return true; return false; } // 
----------------------------------------------------------------------- // 
--- In src/validation.cpp (enforcement with hybrid activation) --- // 
----------------------------------------------------------------------- 
#include <consensus/tx_check.h> #include <versionbits.h> const 
Consensus::Params& params = chainparams.GetConsensus(); int currentHeight = 
chainActive.Tip() ? chainActive.Tip()->nHeight : 0; const bool 
bulk_dust_active = DeploymentActiveAtTip(params, 
Consensus::DEPLOYMENT_BULK_DUST_LIMIT) || (currentHeight >= 
params.BulkDustActivationHeight); if (bulk_dust_active) { if 
(IsBulkDust(tx, currentHeight)) { return 
state.Invalid(TxValidationResult::TX_CONSENSUS, "too-many-tiny-outputs"); } 
} // 
----------------------------------------------------------------------- // 
--- In src/consensus/params.h --- // 
----------------------------------------------------------------------- 
enum DeploymentPos { // ... DEPLOYMENT_BULK_DUST_LIMIT, 
MAX_VERSION_BITS_DEPLOYMENTS }; struct Params { // ... int 
BulkDustActivationHeight; // height flag-day fallback }; // 
----------------------------------------------------------------------- // 
--- In src/chainparams.cpp (per-network values; examples only) --- // 
----------------------------------------------------------------------- 
consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].bit = 12; 
consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nStartTime = 
1767225600; // 2026-01-01 UTC 
consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nTimeout = 
1838160000; // 2028-04-01 UTC 
consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].min_activation_height 
= 969696; consensus.BulkDustActivationHeight = 1021021; // flag-day fallback

-- 
You received this message because you are subscribed to the Google Groups "Bitcoin Development Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bitcoindev+unsubscribe@googlegroups•com.
To view this discussion visit https://groups.google.com/d/msgid/bitcoindev/b0dee827-c2fe-4a68-9b41-f1447ba3c1d3n%40googlegroups.com.

[-- Attachment #1.2: Type: text/html, Size: 26631 bytes --]

^ permalink raw reply	[flat|nested] 3+ messages in thread

* [bitcoindev] Re: By: Doctorbuzz1 {GitHub} Limit "Bulk Dust" with a default filter or consensus.
  2025-10-30  0:03 [bitcoindev] By: Doctorbuzz1 {GitHub} Limit "Bulk Dust" with a default filter or consensus Frenchanfry
@ 2025-10-30  1:02 ` Doctor Buzz
  2025-10-30 14:14   ` Doctor Buzz
  0 siblings, 1 reply; 3+ messages in thread
From: Doctor Buzz @ 2025-10-30  1:02 UTC (permalink / raw)
  To: Bitcoin Development Mailing List


[-- Attachment #1.1: Type: text/plain, Size: 11086 bytes --]

Thanks!  I came here to post it myself.  I just want to point out that it's 
awfully discouraging for a GitHub mod to "close" my 90% developed code, 
asking me to post it elsewhere... but anyway!

Original GitHub post here:
https://github.com/bitcoin/bitcoin/issues/33737#issuecomment-3465288829

The first concept of this with static definition of a "tiny" Tx was posted 
here (with no responses):  
https://bitcoin.stackexchange.com/questions/129139/would-a-bulk-dust-relay-consensus-rule-limiting-100-sub-1-000-sat-outputs-p

Pastebin code probably looks better here than what I can see in the OP of 
this thread:  https://pastebin.com/9qdQCH83
On Wednesday, October 29, 2025 at 7:47:08 PM UTC-5 Frenchanfry wrote:

> A proposal on GitHub I found Highly interesting and a better improvement, 
> dealing with spammers/congestion.
>
> 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 —
> // Place in src/policy/policy.cpp, and call from within IsStandardTx() 
> before returning: // if (IsBulkDust(tx, reason)) // return false; // reject 
> as nonstandard // 
> ========================================================================================================== 
> bool IsBulkDust(const CTransaction& tx, std::string& reason) { static 
> constexpr int MAX_TINY_OUTPUTS = 100; // >=100 tiny outputs triggers ratio 
> check static constexpr double TINY_RATIO_THRESHOLD = 0.6; // >=60% of all 
> outputs tiny → reject static constexpr CAmount BASE_TINY_THRESHOLD = 4096; 
> // starting tiny threshold (sats) static constexpr int64_t 
> FIRST_TINY_HALVING_H = 1260000; // first halving of tiny threshold static 
> constexpr int64_t HALVING_INTERVAL = 210000; // blocks per subsequent 
> halving static constexpr CAmount MIN_TINY_FLOOR = 1; // never below 1 sat 
> const int total = tx.vout.size(); if (total == 0) return false; int 
> currentHeight = chainActive.Tip() ? chainActive.Tip()->nHeight : 0; // Era 
> index for TinyTx threshold, anchored at FIRST_TINY_HALVING_H (not subsidy 
> eras) int era = 0; if (currentHeight >= FIRST_TINY_HALVING_H) { era = 1 + 
> static_cast<int>((currentHeight - FIRST_TINY_HALVING_H) / 
> HALVING_INTERVAL); } CAmount tinyThresh = BASE_TINY_THRESHOLD >> era; // 
> halve per era if (tinyThresh < MIN_TINY_FLOOR) tinyThresh = MIN_TINY_FLOOR; 
> int tiny = 0; for (const auto& out : tx.vout) { if (out.nValue < 
> tinyThresh) ++tiny; } if (tiny >= MAX_TINY_OUTPUTS && 
> (static_cast<double>(tiny) / total) >= TINY_RATIO_THRESHOLD) { reason = 
> strprintf("too-many-tiny-outputs(%d of %d, %.2f%%, tiny<%d)", tiny, total, 
> 100.0 * tiny / total, tinyThresh); return true; // flag as bulk dust 
> (nonstandard) } return false; } 
>
> CONSENSUS (soft-fork, hybrid activation) sketch —
> // Helpers in src/consensus/tx_check.cpp; activation/enforcement in 
> src/validation.cpp // Also define deployment in: src/consensus/params.h, 
> src/chainparams.cpp, src/versionbits.* // 
> ========================================================================================================== 
> // ----------------------------------------------------------------------- 
> // --- In src/consensus/tx_check.cpp (helper only; no params needed) --- // 
> ----------------------------------------------------------------------- 
> static constexpr CAmount BASE_TINY_THRESHOLD = 4096; static constexpr 
> int64_t FIRST_TINY_HALVING_H = 1260000; static constexpr int64_t 
> HALVING_INTERVAL = 210000; static constexpr int MAX_TINY_OUTPUTS = 100; 
> static constexpr double TINY_RATIO_THRESHOLD = 0.6; static constexpr 
> CAmount MIN_TINY_FLOOR = 1; bool IsBulkDust(const CTransaction& tx, int 
> currentHeight) // expose via tx_check.h if needed { const int total = 
> tx.vout.size(); if (total == 0) return false; int era = 0; if 
> (currentHeight >= FIRST_TINY_HALVING_H) { era = 1 + 
> static_cast<int>((currentHeight - FIRST_TINY_HALVING_H) / 
> HALVING_INTERVAL); } CAmount tinyThresh = BASE_TINY_THRESHOLD >> era; if 
> (tinyThresh < MIN_TINY_FLOOR) tinyThresh = MIN_TINY_FLOOR; int tiny = 0; 
> for (const auto& out : tx.vout) { if (out.nValue < tinyThresh) ++tiny; } if 
> (tiny >= MAX_TINY_OUTPUTS && (static_cast<double>(tiny) / total) >= 
> TINY_RATIO_THRESHOLD) return true; return false; } // 
> ----------------------------------------------------------------------- // 
> --- In src/validation.cpp (enforcement with hybrid activation) --- // 
> ----------------------------------------------------------------------- 
> #include <consensus/tx_check.h> #include <versionbits.h> const 
> Consensus::Params& params = chainparams.GetConsensus(); int currentHeight = 
> chainActive.Tip() ? chainActive.Tip()->nHeight : 0; const bool 
> bulk_dust_active = DeploymentActiveAtTip(params, 
> Consensus::DEPLOYMENT_BULK_DUST_LIMIT) || (currentHeight >= 
> params.BulkDustActivationHeight); if (bulk_dust_active) { if 
> (IsBulkDust(tx, currentHeight)) { return 
> state.Invalid(TxValidationResult::TX_CONSENSUS, "too-many-tiny-outputs"); } 
> } // 
> ----------------------------------------------------------------------- // 
> --- In src/consensus/params.h --- // 
> ----------------------------------------------------------------------- 
> enum DeploymentPos { // ... DEPLOYMENT_BULK_DUST_LIMIT, 
> MAX_VERSION_BITS_DEPLOYMENTS }; struct Params { // ... int 
> BulkDustActivationHeight; // height flag-day fallback }; // 
> ----------------------------------------------------------------------- // 
> --- In src/chainparams.cpp (per-network values; examples only) --- // 
> ----------------------------------------------------------------------- 
> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].bit = 12; 
> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nStartTime = 
> 1767225600; // 2026-01-01 UTC 
> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nTimeout = 
> 1838160000; // 2028-04-01 UTC 
> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].min_activation_height 
> = 969696; consensus.BulkDustActivationHeight = 1021021; // flag-day fallback
>

-- 
You received this message because you are subscribed to the Google Groups "Bitcoin Development Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bitcoindev+unsubscribe@googlegroups•com.
To view this discussion visit https://groups.google.com/d/msgid/bitcoindev/71b4d969-8c0d-4ff2-a177-244aa86ca57an%40googlegroups.com.

[-- Attachment #1.2: Type: text/html, Size: 26267 bytes --]

^ permalink raw reply	[flat|nested] 3+ messages in thread

* [bitcoindev] Re: By: Doctorbuzz1 {GitHub} Limit "Bulk Dust" with a default filter or consensus.
  2025-10-30  1:02 ` [bitcoindev] " Doctor Buzz
@ 2025-10-30 14:14   ` Doctor Buzz
  0 siblings, 0 replies; 3+ messages in thread
From: Doctor Buzz @ 2025-10-30 14:14 UTC (permalink / raw)
  To: Bitcoin Development Mailing List


[-- Attachment #1.1: Type: text/plain, Size: 12555 bytes --]

A preemptive response to those who might say that a conservative 
"tiny_count" of 100 "wouldn't do anything":

The point is to add friction without inhibiting any non-data Txs. The image 
of Pepe pumping iron with "UTXO" on top was stored in 1,859 fake pubkeys / 
UTXOs. The proposed tiny_count of 100 would split that particular image 
across at least 19 Txs (likely a lot more if on-chain indexing were used), 
which only adds at least +6% to fees, but it does ruin "atomicity" (images 
all-in-one Tx) by adding complexity of needing some type of index to link 
them, causes confirmation risk, & pushes data abusers toward OP_RETURN or 
witness space.

Changing the 100 tiny_count to 50 ≈ +11% fees; to 30 ≈ +16% fees; & to 20 ≈ 
+24% fees (this only takes into account an extra 200 bytes per additional 
input Tx and does not consider any additional indexing needs) . Perhaps a 
tiny_count could be 20 with a higher ratio of 70%?? ~24% extra fees + added 
complexity could definitely prevent a lot of UTXO abuse.  I was obviously 
just trying to avoid ALL false positives, but there definitely seems like 
there's room to move the tiny_count lower.

On Wednesday, October 29, 2025 at 8:15:08 PM UTC-5 Doctor Buzz wrote:

> Thanks!  I came here to post it myself.  I just want to point out that 
> it's awfully discouraging for a GitHub mod to "close" my 90% developed 
> code, asking me to post it elsewhere... but anyway!
>
> Original GitHub post here:
> https://github.com/bitcoin/bitcoin/issues/33737#issuecomment-3465288829
>
> The first concept of this with static definition of a "tiny" Tx was posted 
> here (with no responses):  
> https://bitcoin.stackexchange.com/questions/129139/would-a-bulk-dust-relay-consensus-rule-limiting-100-sub-1-000-sat-outputs-p
>
> Pastebin code probably looks better here than what I can see in the OP of 
> this thread:  https://pastebin.com/9qdQCH83
> On Wednesday, October 29, 2025 at 7:47:08 PM UTC-5 Frenchanfry wrote:
>
>> A proposal on GitHub I found Highly interesting and a better improvement, 
>> dealing with spammers/congestion.
>>
>> 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 —
>> // Place in src/policy/policy.cpp, and call from within IsStandardTx() 
>> before returning: // if (IsBulkDust(tx, reason)) // return false; // reject 
>> as nonstandard // 
>> ========================================================================================================== 
>> bool IsBulkDust(const CTransaction& tx, std::string& reason) { static 
>> constexpr int MAX_TINY_OUTPUTS = 100; // >=100 tiny outputs triggers ratio 
>> check static constexpr double TINY_RATIO_THRESHOLD = 0.6; // >=60% of all 
>> outputs tiny → reject static constexpr CAmount BASE_TINY_THRESHOLD = 4096; 
>> // starting tiny threshold (sats) static constexpr int64_t 
>> FIRST_TINY_HALVING_H = 1260000; // first halving of tiny threshold static 
>> constexpr int64_t HALVING_INTERVAL = 210000; // blocks per subsequent 
>> halving static constexpr CAmount MIN_TINY_FLOOR = 1; // never below 1 sat 
>> const int total = tx.vout.size(); if (total == 0) return false; int 
>> currentHeight = chainActive.Tip() ? chainActive.Tip()->nHeight : 0; // Era 
>> index for TinyTx threshold, anchored at FIRST_TINY_HALVING_H (not subsidy 
>> eras) int era = 0; if (currentHeight >= FIRST_TINY_HALVING_H) { era = 1 + 
>> static_cast<int>((currentHeight - FIRST_TINY_HALVING_H) / 
>> HALVING_INTERVAL); } CAmount tinyThresh = BASE_TINY_THRESHOLD >> era; // 
>> halve per era if (tinyThresh < MIN_TINY_FLOOR) tinyThresh = MIN_TINY_FLOOR; 
>> int tiny = 0; for (const auto& out : tx.vout) { if (out.nValue < 
>> tinyThresh) ++tiny; } if (tiny >= MAX_TINY_OUTPUTS && 
>> (static_cast<double>(tiny) / total) >= TINY_RATIO_THRESHOLD) { reason = 
>> strprintf("too-many-tiny-outputs(%d of %d, %.2f%%, tiny<%d)", tiny, total, 
>> 100.0 * tiny / total, tinyThresh); return true; // flag as bulk dust 
>> (nonstandard) } return false; } 
>>
>> CONSENSUS (soft-fork, hybrid activation) sketch —
>> // Helpers in src/consensus/tx_check.cpp; activation/enforcement in 
>> src/validation.cpp // Also define deployment in: src/consensus/params.h, 
>> src/chainparams.cpp, src/versionbits.* // 
>> ========================================================================================================== 
>> // ----------------------------------------------------------------------- 
>> // --- In src/consensus/tx_check.cpp (helper only; no params needed) --- // 
>> ----------------------------------------------------------------------- 
>> static constexpr CAmount BASE_TINY_THRESHOLD = 4096; static constexpr 
>> int64_t FIRST_TINY_HALVING_H = 1260000; static constexpr int64_t 
>> HALVING_INTERVAL = 210000; static constexpr int MAX_TINY_OUTPUTS = 100; 
>> static constexpr double TINY_RATIO_THRESHOLD = 0.6; static constexpr 
>> CAmount MIN_TINY_FLOOR = 1; bool IsBulkDust(const CTransaction& tx, int 
>> currentHeight) // expose via tx_check.h if needed { const int total = 
>> tx.vout.size(); if (total == 0) return false; int era = 0; if 
>> (currentHeight >= FIRST_TINY_HALVING_H) { era = 1 + 
>> static_cast<int>((currentHeight - FIRST_TINY_HALVING_H) / 
>> HALVING_INTERVAL); } CAmount tinyThresh = BASE_TINY_THRESHOLD >> era; if 
>> (tinyThresh < MIN_TINY_FLOOR) tinyThresh = MIN_TINY_FLOOR; int tiny = 0; 
>> for (const auto& out : tx.vout) { if (out.nValue < tinyThresh) ++tiny; } if 
>> (tiny >= MAX_TINY_OUTPUTS && (static_cast<double>(tiny) / total) >= 
>> TINY_RATIO_THRESHOLD) return true; return false; } // 
>> ----------------------------------------------------------------------- // 
>> --- In src/validation.cpp (enforcement with hybrid activation) --- // 
>> ----------------------------------------------------------------------- 
>> #include <consensus/tx_check.h> #include <versionbits.h> const 
>> Consensus::Params& params = chainparams.GetConsensus(); int currentHeight = 
>> chainActive.Tip() ? chainActive.Tip()->nHeight : 0; const bool 
>> bulk_dust_active = DeploymentActiveAtTip(params, 
>> Consensus::DEPLOYMENT_BULK_DUST_LIMIT) || (currentHeight >= 
>> params.BulkDustActivationHeight); if (bulk_dust_active) { if 
>> (IsBulkDust(tx, currentHeight)) { return 
>> state.Invalid(TxValidationResult::TX_CONSENSUS, "too-many-tiny-outputs"); } 
>> } // 
>> ----------------------------------------------------------------------- // 
>> --- In src/consensus/params.h --- // 
>> ----------------------------------------------------------------------- 
>> enum DeploymentPos { // ... DEPLOYMENT_BULK_DUST_LIMIT, 
>> MAX_VERSION_BITS_DEPLOYMENTS }; struct Params { // ... int 
>> BulkDustActivationHeight; // height flag-day fallback }; // 
>> ----------------------------------------------------------------------- // 
>> --- In src/chainparams.cpp (per-network values; examples only) --- // 
>> ----------------------------------------------------------------------- 
>> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].bit = 12; 
>> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nStartTime = 
>> 1767225600; // 2026-01-01 UTC 
>> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nTimeout = 
>> 1838160000; // 2028-04-01 UTC 
>> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].min_activation_height 
>> = 969696; consensus.BulkDustActivationHeight = 1021021; // flag-day fallback
>>
>

-- 
You received this message because you are subscribed to the Google Groups "Bitcoin Development Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bitcoindev+unsubscribe@googlegroups•com.
To view this discussion visit https://groups.google.com/d/msgid/bitcoindev/15849db6-67af-4a03-86a4-bb5a288b9ad1n%40googlegroups.com.

[-- Attachment #1.2: Type: text/html, Size: 28754 bytes --]

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2025-10-30 14:20 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-10-30  0:03 [bitcoindev] By: Doctorbuzz1 {GitHub} Limit "Bulk Dust" with a default filter or consensus Frenchanfry
2025-10-30  1:02 ` [bitcoindev] " Doctor Buzz
2025-10-30 14:14   ` Doctor Buzz

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox