I’m exploring potential solutions to stop the UTXO bloat pattern without touching scripting or monitoring semantics. This rule only flags “bulk dust” transactions that create a lot of very low value output.
- Threshold: At least 100 outputs with value less than 1,000 saturation
- Ratio: Their “small” (less than 1,000 satellites) power is more than 60% of the total Tx power.
The goal is not to eliminate all arbitrary data usage, but to increase the cost of the patterns most relevant to UTXO growth (cheap “bulk dust” Tx) while leaving regular payments, channel openings, and one-time registrations unaffected.
pattern of this happening limit (does not necessarily have to be excluded)
- Fake public keys that hide data (UTXO fan-out)
- Bitcoin STAMPS/UTXO-art with heavy use of dust UTXO
- BRC-20 Batch mint to fan out small outputs
- Several batched Ordinal inscriptions that spread data/state over many small UTXOs
- Dust Bombing (Tracking) / UTXO Illegal Occupation / Resource Depletion Attempt
- Massive micro airdrops with less than 1k-sat power (side effect)
Not targeted/not targeted
- Single or small number of output inscriptions with large amounts of witness data (these do not trigger the “bulk dust” heuristic)
- A scheme that simply uses 1,000 or more Sats per output (economically expensive, but still valid)
Why ratio + count?
Requiring both (tiny_count ≥ 100) and (tiny_count / total_outputs ≥ 0.6) reduces false positives (such as large stored batch payments with mixed values). It mainly focuses on transactions that consist of dusty output.
listen
- Are there any reliable non-spam use cases that require 100 or more sub-1k-sat outputs with a small ratio of 60% or more in one TX?
- Are there any policy pitfalls I’m overlooking (e.g. fee market dynamics, strange Coinbase behavior, privacy tools)?
- Is there any prior art or measurement results available regarding the prevalence of such transactions?
(Click here for syntax highlighting: https://pastebin.com/tYsvDh2R)
Sketching a relay policy filter —
// Place in /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 CAmount MIN_OUTPUT_VALUE_SATS = 1000; // < 1000 sats counts as "tiny"
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
int tiny = 0;
const int total = tx.vout.size();
// Sanity check — avoid division by zero
if (total == 0)
return false;
// Count any spendable output under 1000 sats as "tiny"
for (const auto& out : tx.vout) {
if (out.nValue < MIN_OUTPUT_VALUE_SATS)
++tiny;
}
// Threshold + ratio check
if (tiny >= MAX_TINY_OUTPUTS && (static_cast(tiny) / total) >= TINY_RATIO_THRESHOLD)
{
reason = strprintf("too-many-tiny-outputs(%d of %d, %.2f%%)", tiny, total, 100.0 * tiny / total);
return true; // flag as bulk dust
}
return false;
}
Sketching consensus (soft fork, hybrid activation) —
// Helpers in /consensus/tx_check.cpp; activation/enforcement in /validation.cpp
// Also define deployment in: /consensus/params.h, /chainparams.cpp, /versionbits.*
// -----------------------------------------------------------------------
// --- In /consensus/tx_check.cpp (helper only; no params needed) ---
// -----------------------------------------------------------------------
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 check
static constexpr double TINY_RATIO_THRESHOLD = 0.6; // >= 60% of all outputs tiny = reject
bool IsBulkDust(const CTransaction& tx) // expose via tx_check.h if needed
{
int tiny = 0;
const int total = tx.vout.size();
// Sanity check — avoid division by zero
if (total == 0)
return false;
// Count any spendable output under 1000 sats as "tiny"
for (const auto& out : tx.vout) {
if (out.nValue < MIN_OUTPUT_VALUE_SATS)
++tiny;
}
// Threshold + ratio check
if (tiny >= MAX_TINY_OUTPUTS && ((static_cast(tiny) / total) >= TINY_RATIO_THRESHOLD))
return true;
return false;
}
// -----------------------------------------------------------------------
// --- In /validation.cpp (enforcement with hybrid activation) ---
// -----------------------------------------------------------------------
#include
#include
// ... inside the appropriate validation path (e.g., after basic tx checks),
// with access 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; // height flag-day fallback
};
// -----------------------------------------------------------------------
// --- In /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
Discover more from Earlybirds Invest
Subscribe to get the latest posts sent to your email.

