I’m exploring a possible resolution to discourage UTXO-bloat patterns with out touching Script or witness semantics. This rule merely flags “bulk mud” transactions that create many very low-value outputs:
- Threshold: not less than 100 outputs with worth < 1,000 sats
- Ratio: these “tiny” (sub-1,000 sat) outputs are ≥ 60% of all outputs within the Tx
The purpose isn’t to get rid of all arbitrary information makes use of, however to lift prices for patterns most related to UTXO progress (low cost, “bulk mud” Txs), whereas leaving typical funds, channel opens, and one-off inscriptions unaffected.
Patterns this is able to restrict (not essentially get rid of)
- Faux pubkeys hiding information (UTXO fan-outs)
- Bitcoin STAMPS / UTXO-art utilizing many mud UTXOs
- BRC-20 batch mints that fan out tiny outputs
- Some batched Ordinal inscriptions that unfold information/state throughout many small UTXOs
- Mud bombing (monitoring) / UTXO squatting / resource-exhaustion makes an attempt
- Mass micro-airdrops of sub-1k-sat outputs (collateral impact)
Non-goals / Not coated
- Single/few-output inscriptions with massive witness information (these wouldn’t set off the “bulk mud” heuristic)
- Any scheme that merely makes use of ≥1,000 sat per output (economically costlier however nonetheless legitimate)
Why a ratio + depend?
Requiring each (tiny_count ≥ 100) and (tiny_count / total_outputs ≥ 0.6) reduces false positives (e.g., massive custodial batch payouts with a mixture of values). It focuses on transactions which might be largely product of dust-like outputs.
Ask
- Are there credible, non-spam use-cases that want ≥100 sub-1k-sat outputs with ≥60% tiny ratio in a single tx?
- Are there coverage pitfalls I’m lacking (e.g., payment market dynamics, odd coinbase behaviors, privateness instruments)?
- Any prior artwork or measurements you possibly can level to in regards to the prevalence of such transactions?
(with syntax highlighting right here: https://pastebin.com/tYsvDh2R)
RELAY POLICY FILTER sketch —
// Place in /coverage/coverage.cpp, and name from inside IsStandardTx() earlier than returning:
// if (IsBulkDust(tx, motive))
// return false; // reject as nonstandard
bool IsBulkDust(const CTransaction& tx, std::string& motive)
{
static constexpr CAmount MIN_OUTPUT_VALUE_SATS = 1000; // < 1000 sats counts as "tiny"
static constexpr int MAX_TINY_OUTPUTS = 100; // >= 100 tiny outputs triggers ratio examine
static constexpr double TINY_RATIO_THRESHOLD = 0.6; // >= 60% of all outputs tiny = reject
int tiny = 0;
const int whole = tx.vout.dimension();
// Sanity examine — keep away from division by zero
if (whole == 0)
return false;
// Rely any spendable output beneath 1000 sats as "tiny"
for (const auto& out : tx.vout) {
if (out.nValue < MIN_OUTPUT_VALUE_SATS)
++tiny;
}
// Threshold + ratio examine
if (tiny >= MAX_TINY_OUTPUTS && (static_cast(tiny) / whole) >= TINY_RATIO_THRESHOLD)
{
motive = strprintf("too-many-tiny-outputs(%d of %d, %.2f%%)", tiny, whole, 100.0 * tiny / whole);
return true; // flag as bulk mud
}
return false;
}
CONSENSUS (soft-fork, hybrid activation) sketch —
// Helpers in /consensus/tx_check.cpp; activation/enforcement in /validation.cpp
// Additionally outline deployment in: /consensus/params.h, /chainparams.cpp, /versionbits.*
// -----------------------------------------------------------------------
// --- In /consensus/tx_check.cpp (helper solely; no params wanted) ---
// -----------------------------------------------------------------------
static constexpr CAmount MIN_OUTPUT_VALUE_SATS = 1000; // < 1000 sats counts as "tiny"
static constexpr int MAX_TINY_OUTPUTS = 100; // >= 100 tiny outputs triggers ratio examine
static constexpr double TINY_RATIO_THRESHOLD = 0.6; // >= 60% of all outputs tiny = reject
bool IsBulkDust(const CTransaction& tx) // expose through tx_check.h if wanted
{
int tiny = 0;
const int whole = tx.vout.dimension();
// Sanity examine — keep away from division by zero
if (whole == 0)
return false;
// Rely any spendable output beneath 1000 sats as "tiny"
for (const auto& out : tx.vout) {
if (out.nValue < MIN_OUTPUT_VALUE_SATS)
++tiny;
}
// Threshold + ratio examine
if (tiny >= MAX_TINY_OUTPUTS && ((static_cast(tiny) / whole) >= TINY_RATIO_THRESHOLD))
return true;
return false;
}
// -----------------------------------------------------------------------
// --- In /validation.cpp (enforcement with hybrid activation) ---
// -----------------------------------------------------------------------
#embrace
#embrace
// ... inside the suitable validation path (e.g., after primary tx checks),
// with entry to chainparams/params and a tip pointer:
const Consensus::Params& params = chainparams.GetConsensus();
const bool bulk_dust_active =
DeploymentActiveAtTip(params, Consensus::DEPLOYMENT_BULK_DUST_LIMIT) ||
(chainActive.Tip() && chainActive.Tip()->nHeight >= params.BulkDustActivationHeight);
if (bulk_dust_active) {
if (IsBulkDust(tx)) {
return state.Invalid(TxValidationResult::TX_CONSENSUS, "too-many-tiny-outputs");
}
}
// -----------------------------------------------------------------------
// --- In /consensus/params.h ---
// -----------------------------------------------------------------------
enum DeploymentPos {
// ...
DEPLOYMENT_BULK_DUST_LIMIT,
MAX_VERSION_BITS_DEPLOYMENTS
};
struct Params {
// ...
int BulkDustActivationHeight; // top flag-day fallback
};
// -----------------------------------------------------------------------
// --- In /chainparams.cpp (per-network values; examples solely) ---
// -----------------------------------------------------------------------
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

