From mboxrd@z Thu Jan 1 00:00:00 1970 Delivery-date: Thu, 30 Oct 2025 07:20:10 -0700 Received: from mail-oo1-f64.google.com ([209.85.161.64]) by mail.fairlystable.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.94.2) (envelope-from ) id 1vETVY-0005TP-TX for bitcoindev@gnusha.org; Thu, 30 Oct 2025 07:20:10 -0700 Received: by mail-oo1-f64.google.com with SMTP id 006d021491bc7-65681fd25b0sf1377193eaf.3 for ; Thu, 30 Oct 2025 07:20:08 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=googlegroups.com; s=20230601; t=1761834003; x=1762438803; darn=gnusha.org; h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post :list-id:mailing-list:precedence:x-original-sender:mime-version :subject:references:in-reply-to:message-id:to:from:date:sender:from :to:cc:subject:date:message-id:reply-to; bh=/O9FybxUzLE37tl2ihlIw3uczNPqlguMGWGYhDP599A=; b=YOP/ZCavI/xfA//gDVSp6aDr2msYcz0aX2zQAaVuxCoU9ImZK2ICpiW/Wi1mOp7524 LDIb24w1PfTKH+yHW07I8ZXBs6H0PTxm2Wr9BFcx7jiM6oSg/1s1sopAhSLJjA0uYwZ2 7097uU+8AlBP/aiu3X2g/sxycVdvwj9nfzsgwPbajhZPmSSAdVQOjZ8rEnlZSTzzHM9T Z6piZ4Y2GuuE5cYfxDJ5XGolxxhqtCmB3ULUCm+cO4ozSqgoVi+UP7llAQTY/CH52tNZ ZwLU9EWwyYZnfaGU4lEM2f0/Uo4iuNOWoSyKT3uy87Fa25sNOGDGRsR50eCk8M8W9oa9 K1ZQ== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1761834003; x=1762438803; darn=gnusha.org; h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post :list-id:mailing-list:precedence:x-original-sender:mime-version :subject:references:in-reply-to:message-id:to:from:date:from:to:cc :subject:date:message-id:reply-to; bh=/O9FybxUzLE37tl2ihlIw3uczNPqlguMGWGYhDP599A=; b=Nuo58ILXtBTAzgSqE2Otp0MnfXIcHP2Wx0s5x6iYeQr70MvNqk9Fn4jVk1lF7vpzBE FmXOqTzT5CQTYq9dllf/A4nEF2kNblsPWGBobYdjflftHH117TDj9vT+IWUbELP+uyGW l0YhS4tad2M3d478120dororCh/u45NwA36SAOSGXZVhCH3fjN5bHeernJlM99A9Hotn 3U2pk3mppsHdO1QyQB+azEEG+bRKhbUR9LAULpFQwv8PQbxQZpR9VhL2ATnEF4ARd9u8 IMJ6R4zHI7zQcmeprFyq5k0AEpPYPcsV860GI2QZgpZk1eiMaYzcrL56yP/AJisxVhyL ODBA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1761834003; x=1762438803; h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post :list-id:mailing-list:precedence:x-original-sender:mime-version :subject:references:in-reply-to:message-id:to:from:date:x-beenthere :x-gm-message-state:sender:from:to:cc:subject:date:message-id :reply-to; bh=/O9FybxUzLE37tl2ihlIw3uczNPqlguMGWGYhDP599A=; b=ikPW8QnrYFC8yxxMzHv/87XgEWfFfxyWpVWTiFCKmU1MefQjYYj0OB84d08o/nQaoZ Tc+AZD+VIHY+Eu0JzgCRaNbfxhKulYmdT9QXKQbE2nby+T7FUfPFljXB9vw6i1iwykYa 8ql1giwKx4zH7pYbyQluxdWudWDt7Ro0GcxNp3h2LxlEM8mfCkZgzW305wACfdqJe1yk j9gYCBDSkvsv2LmCsNuV4RV8jtF5+YfrmmhSJA3CnOtx4vSVG3VZPjNurBlihpx3Rwr6 M6h6WIG79Y60XONzlaNC6RFGy1qNUE3VJlqhdE24+GDG3Dx0KM8f6rFtQ7hkM6KP2OSZ puZw== Sender: bitcoindev@googlegroups.com X-Forwarded-Encrypted: i=1; AJvYcCUCJo+kFjG+wg806V50Z0kq3b40kEcdFaYk5EVaOetuUpa+4jUU4tLxKflwW6Eo6wYd+kJ4o+0fmb7g@gnusha.org X-Gm-Message-State: AOJu0YwfbDaptr7qTl/8ZOYA6126Yw64TOgCXe6PUua2GWgdUQ85g4xk /tz/HRc4ckJ/2lzHd7/u4jTvsz+5EO1D7nvcfvwjgKpd6F2osqX5O4s3 X-Google-Smtp-Source: AGHT+IGWJZGgLezpfxdqdlboApVu4I2LCsRDdyl+KQQcPJqO+OdYGh3/djODs2GBwPuuMy2gnDtnlg== X-Received: by 2002:a05:6808:250e:b0:441:8f74:f12 with SMTP id 5614622812f47-44f7a5944f2mr3179452b6e.60.1761834002051; Thu, 30 Oct 2025 07:20:02 -0700 (PDT) X-BeenThere: bitcoindev@googlegroups.com; h="Ae8XA+aAqiRUtUTzCUUBNz7Ji30EnUmFr1/tiU1G49+FHbCJAA==" Received: by 2002:a4a:dd8f:0:b0:651:c080:3bca with SMTP id 006d021491bc7-656823c873als288357eaf.0.-pod-prod-09-us; Thu, 30 Oct 2025 07:19:56 -0700 (PDT) X-Received: by 2002:a05:6808:4e1b:b0:44f:8bff:436d with SMTP id 5614622812f47-44f8bff498bmr1040803b6e.2.1761833996507; Thu, 30 Oct 2025 07:19:56 -0700 (PDT) Received: by 2002:a05:690c:a5c1:b0:74f:1486:e2a9 with SMTP id 00721157ae682-78629398ee3ms7b3; Thu, 30 Oct 2025 07:14:34 -0700 (PDT) X-Received: by 2002:a05:690c:3204:b0:784:a898:1788 with SMTP id 00721157ae682-786390d3f6fmr35017267b3.32.1761833672610; Thu, 30 Oct 2025 07:14:32 -0700 (PDT) Date: Thu, 30 Oct 2025 07:14:32 -0700 (PDT) From: Doctor Buzz To: Bitcoin Development Mailing List Message-Id: <15849db6-67af-4a03-86a4-bb5a288b9ad1n@googlegroups.com> In-Reply-To: <71b4d969-8c0d-4ff2-a177-244aa86ca57an@googlegroups.com> References: <71b4d969-8c0d-4ff2-a177-244aa86ca57an@googlegroups.com> Subject: [bitcoindev] Re: By: Doctorbuzz1 {GitHub} Limit "Bulk Dust" with a default filter or consensus. MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_Part_2358_1802183752.1761833672298" X-Original-Sender: BuzzHeavy@gmail.com Precedence: list Mailing-list: list bitcoindev@googlegroups.com; contact bitcoindev+owners@googlegroups.com List-ID: X-Google-Group-Id: 786775582512 List-Post: , List-Help: , List-Archive: , List-Unsubscribe: , X-Spam-Score: -0.5 (/) ------=_Part_2358_1802183752.1761833672298 Content-Type: multipart/alternative; boundary="----=_Part_2359_1651605.1761833672298" ------=_Part_2359_1651605.1761833672298 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable A preemptive response to those who might say that a conservative=20 "tiny_count" of 100 "wouldn't do anything": The point is to add friction without inhibiting any non-data Txs. The image= =20 of Pepe pumping iron with "UTXO" on top was stored in 1,859 fake pubkeys /= =20 UTXOs. The proposed tiny_count of 100 would split that particular image=20 across at least 19 Txs (likely a lot more if on-chain indexing were used),= =20 which only adds at least +6% to fees, but it does ruin "atomicity" (images= =20 all-in-one Tx) by adding complexity of needing some type of index to link= =20 them, causes confirmation risk, & pushes data abusers toward OP_RETURN or= =20 witness space. Changing the 100 tiny_count to 50 =E2=89=88 +11% fees; to 30 =E2=89=88 +16%= fees; & to 20 =E2=89=88=20 +24% fees (this only takes into account an extra 200 bytes per additional= =20 input Tx and does not consider any additional indexing needs) . Perhaps a= =20 tiny_count could be 20 with a higher ratio of 70%?? ~24% extra fees + added= =20 complexity could definitely prevent a lot of UTXO abuse. I was obviously= =20 just trying to avoid ALL false positives, but there definitely seems like= =20 there's room to move the tiny_count lower. On Wednesday, October 29, 2025 at 8:15:08=E2=80=AFPM UTC-5 Doctor Buzz wrot= e: > Thanks! I came here to post it myself. I just want to point out that=20 > it's awfully discouraging for a GitHub mod to "close" my 90% developed=20 > 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 poste= d=20 > here (with no responses): =20 > https://bitcoin.stackexchange.com/questions/129139/would-a-bulk-dust-rela= y-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= =20 > this thread: https://pastebin.com/9qdQCH83 > On Wednesday, October 29, 2025 at 7:47:08=E2=80=AFPM UTC-5 Frenchanfry wr= ote: > >> A proposal on GitHub I found Highly interesting and a better improvement= ,=20 >> dealing with spammers/congestion. >> >> I=E2=80=99m exploring a potential default filter or consensus-level rule= (since a=20 >> large number of people believe that default filters don't work) to=20 >> discourage UTXO-bloat patterns without touching Script, witness data, or= =20 >> the block size limit. >> >> The idea is to target =E2=80=9Cbulk dust=E2=80=9D transactions =E2=80=94= those that create large=20 >> numbers of extremely small outputs =E2=80=94 which are the main cause of= long-term=20 >> UTXO set growth. >> >> These types of "bulk dust" transactions have been the No. 1 reason cited= =20 >> for wanting to expand the default OP_RETURN limit... and removing that= =20 >> limit obviously influenced BIP 444. So it appears to me that there is=20 >> overwhelming majority support for limiting these types of "bulk dust"=20 >> transactions, as they do present a legitimate concern for node runners. >> >> Concept >> >> Flag a transaction as =E2=80=9Cbulk dust=E2=80=9D if: >> >> - It has >=3D100 outputs each below a dynamically defined TinyTx=20 >> threshold, and >> - Those tiny outputs make up >=3D60% of all outputs in the transactio= n. >> >> When flagged, it would be considered nonstandard (relay policy) or=20 >> invalid (if soft-forked into consensus). >> >> TinyTx threshold (dynamic halving schedule) >> >> I originally considered a constant definition of what was a "tiny" Tx to= =20 >> be 1,000 sats... but some might still just use 1,001 sats, right? Plus= =20 >> there very likely will be a time where there is a valid use-case of >100= =20 >> outputs under 1,000 sats. >> >> Rather than fixing the =E2=80=9Ctiny=E2=80=9D threshold to a constant li= ke 1,000 sats,=20 >> the rule defines it as a decreasing function of block height, starting h= igh=20 >> 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 >> =E2=80=A6 -- every 210,000 blocks -- =E2=80=A6 until 1 sat floor >> >> This gradual halving ensures the definition of "tiny" stays relevant as= =20 >> Bitcoin=E2=80=99s value rises. >> For example, if 1 sat =3D $1 someday, having 100 outputs worth <1,000 sa= ts=20 >> each would no longer represent spam =E2=80=94 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=20 >> dust outputs >> - BRC-20 batch mints with 100s of "tiny" sat fan-outs >> - Some Ordinal or state inscription schemes that distribute data=20 >> 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 =E2=80=94 making them se= veral=20 >> 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=20 >> 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 >=3D 100) and (tiny_ratio >=3D 60%) helps avo= id=20 >> false positives, such as legitimate custodial payouts or consolidation= =20 >> transactions with mixed values. >> It specifically filters transactions that are mostly dust, rather than= =20 >> merely containing some. >> >> Inquiry >> >> - Are there credible, non-spam use cases that truly require >=3D100= =20 >> sub-4k-sat outputs (or equivalent at later eras) and a >=3D60% tiny r= atio? >> - Could this affect fee market behavior or any privacy tools in=20 >> 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=E2=80=99t censor any monetary transaction or prevent= =20 >> inscriptions; it simply prices storage according to resource cost. >> It keeps the chain =E2=80=9Clight and nimble=E2=80=9D for everyday payme= nts while=20 >> allowing future flexibility =E2=80=94 because the TinyTx definition decr= eases=20 >> automatically in line with halvings and Bitcoin=E2=80=99s long-term valu= e growth. >> >> CODE SKETCHES >> (with minimal syntax highlighting here: https://pastebin.com/9qdQCH83) >> >> RELAY POLICY FILTER sketch =E2=80=94 >> // Place in src/policy/policy.cpp, and call from within IsStandardTx()= =20 >> before returning: // if (IsBulkDust(tx, reason)) // return false; // rej= ect=20 >> as nonstandard //=20 >> =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=20 >> bool IsBulkDust(const CTransaction& tx, std::string& reason) { static=20 >> constexpr int MAX_TINY_OUTPUTS =3D 100; // >=3D100 tiny outputs triggers= ratio=20 >> check static constexpr double TINY_RATIO_THRESHOLD =3D 0.6; // >=3D60% o= f all=20 >> outputs tiny =E2=86=92 reject static constexpr CAmount BASE_TINY_THRESHO= LD =3D 4096;=20 >> // starting tiny threshold (sats) static constexpr int64_t=20 >> FIRST_TINY_HALVING_H =3D 1260000; // first halving of tiny threshold sta= tic=20 >> constexpr int64_t HALVING_INTERVAL =3D 210000; // blocks per subsequent= =20 >> halving static constexpr CAmount MIN_TINY_FLOOR =3D 1; // never below 1 = sat=20 >> const int total =3D tx.vout.size(); if (total =3D=3D 0) return false; in= t=20 >> currentHeight =3D chainActive.Tip() ? chainActive.Tip()->nHeight : 0; //= Era=20 >> index for TinyTx threshold, anchored at FIRST_TINY_HALVING_H (not subsid= y=20 >> eras) int era =3D 0; if (currentHeight >=3D FIRST_TINY_HALVING_H) { era = =3D 1 +=20 >> static_cast((currentHeight - FIRST_TINY_HALVING_H) /=20 >> HALVING_INTERVAL); } CAmount tinyThresh =3D BASE_TINY_THRESHOLD >> era; = //=20 >> halve per era if (tinyThresh < MIN_TINY_FLOOR) tinyThresh =3D MIN_TINY_F= LOOR;=20 >> int tiny =3D 0; for (const auto& out : tx.vout) { if (out.nValue <=20 >> tinyThresh) ++tiny; } if (tiny >=3D MAX_TINY_OUTPUTS &&=20 >> (static_cast(tiny) / total) >=3D TINY_RATIO_THRESHOLD) { reason = =3D=20 >> strprintf("too-many-tiny-outputs(%d of %d, %.2f%%, tiny<%d)", tiny, tota= l,=20 >> 100.0 * tiny / total, tinyThresh); return true; // flag as bulk dust=20 >> (nonstandard) } return false; }=20 >> >> CONSENSUS (soft-fork, hybrid activation) sketch =E2=80=94 >> // Helpers in src/consensus/tx_check.cpp; activation/enforcement in=20 >> src/validation.cpp // Also define deployment in: src/consensus/params.h,= =20 >> src/chainparams.cpp, src/versionbits.* //=20 >> =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=20 >> // ---------------------------------------------------------------------= --=20 >> // --- In src/consensus/tx_check.cpp (helper only; no params needed) ---= //=20 >> -----------------------------------------------------------------------= =20 >> static constexpr CAmount BASE_TINY_THRESHOLD =3D 4096; static constexpr= =20 >> int64_t FIRST_TINY_HALVING_H =3D 1260000; static constexpr int64_t=20 >> HALVING_INTERVAL =3D 210000; static constexpr int MAX_TINY_OUTPUTS =3D 1= 00;=20 >> static constexpr double TINY_RATIO_THRESHOLD =3D 0.6; static constexpr= =20 >> CAmount MIN_TINY_FLOOR =3D 1; bool IsBulkDust(const CTransaction& tx, in= t=20 >> currentHeight) // expose via tx_check.h if needed { const int total =3D= =20 >> tx.vout.size(); if (total =3D=3D 0) return false; int era =3D 0; if=20 >> (currentHeight >=3D FIRST_TINY_HALVING_H) { era =3D 1 +=20 >> static_cast((currentHeight - FIRST_TINY_HALVING_H) /=20 >> HALVING_INTERVAL); } CAmount tinyThresh =3D BASE_TINY_THRESHOLD >> era; = if=20 >> (tinyThresh < MIN_TINY_FLOOR) tinyThresh =3D MIN_TINY_FLOOR; int tiny = =3D 0;=20 >> for (const auto& out : tx.vout) { if (out.nValue < tinyThresh) ++tiny; }= if=20 >> (tiny >=3D MAX_TINY_OUTPUTS && (static_cast(tiny) / total) >=3D= =20 >> TINY_RATIO_THRESHOLD) return true; return false; } //=20 >> ----------------------------------------------------------------------- = //=20 >> --- In src/validation.cpp (enforcement with hybrid activation) --- //=20 >> -----------------------------------------------------------------------= =20 >> #include #include const=20 >> Consensus::Params& params =3D chainparams.GetConsensus(); int currentHei= ght =3D=20 >> chainActive.Tip() ? chainActive.Tip()->nHeight : 0; const bool=20 >> bulk_dust_active =3D DeploymentActiveAtTip(params,=20 >> Consensus::DEPLOYMENT_BULK_DUST_LIMIT) || (currentHeight >=3D=20 >> params.BulkDustActivationHeight); if (bulk_dust_active) { if=20 >> (IsBulkDust(tx, currentHeight)) { return=20 >> state.Invalid(TxValidationResult::TX_CONSENSUS, "too-many-tiny-outputs")= ; }=20 >> } //=20 >> ----------------------------------------------------------------------- = //=20 >> --- In src/consensus/params.h --- //=20 >> -----------------------------------------------------------------------= =20 >> enum DeploymentPos { // ... DEPLOYMENT_BULK_DUST_LIMIT,=20 >> MAX_VERSION_BITS_DEPLOYMENTS }; struct Params { // ... int=20 >> BulkDustActivationHeight; // height flag-day fallback }; //=20 >> ----------------------------------------------------------------------- = //=20 >> --- In src/chainparams.cpp (per-network values; examples only) --- //=20 >> -----------------------------------------------------------------------= =20 >> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].bit =3D 12= ;=20 >> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nStartTime= =3D=20 >> 1767225600; // 2026-01-01 UTC=20 >> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nTimeout = =3D=20 >> 1838160000; // 2028-04-01 UTC=20 >> consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].min_activa= tion_height=20 >> =3D 969696; consensus.BulkDustActivationHeight =3D 1021021; // flag-day = fallback >> > --=20 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 e= mail to bitcoindev+unsubscribe@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/bitcoindev/= 15849db6-67af-4a03-86a4-bb5a288b9ad1n%40googlegroups.com. ------=_Part_2359_1651605.1761833672298 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable A preemptive response to those who might say that a conservative "tiny_coun= t" of 100 "wouldn't do anything":

The point is to add friction w= ithout inhibiting any non-data Txs. The image of Pepe pumping iron with "UT= XO" on top was stored in 1,859 fake pubkeys / UTXOs. The proposed tiny_coun= t 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 complex= ity of needing some type of index to link them, causes confirmation risk, &= amp; pushes data abusers toward OP_RETURN or witness space.

Chan= ging the 100 tiny_count to 50 =E2=89=88 +11% fees; to 30 =E2=89=88 +16% fee= s; & to 20 =E2=89=88 +24% fees (this only takes into account an extra 2= 00 bytes per additional input Tx and does not consider any additional index= ing 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 a= buse.=C2=A0 I was obviously just trying to avoid ALL false positives, but t= here definitely seems like there's room to move the tiny_count lower.
=
On = Wednesday, October 29, 2025 at 8:15:08=E2=80=AFPM UTC-5 Doctor Buzz wrote:<= br/>
Thanks!=C2=A0= I came here to post it myself.=C2=A0 I just want to point out that it'= s awfully discouraging for a GitHub mod to "close" my 90% develop= ed code, asking me to post it elsewhere... but anyway!

Original GitH= ub post here:
https://gith= ub.com/bitcoin/bitcoin/issues/33737#issuecomment-3465288829

The = first concept of this with static definition of a "tiny" Tx was p= osted here (with no responses):=C2=A0 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:=C2=A0=C2=A0https://pastebin.com/9qdQCH83
On Wednesday, October 29, 2025 at 7:47= :08=E2=80=AFPM UTC-5 Frenchanfry wrote:
A proposal on GitHub I found Highly interesting and a bet= ter improvement, dealing with spammers/congestion.

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

The idea is to target =E2=80=9Cbulk du= st=E2=80=9D transactions =E2=80=94 those that create large numbers of extre= mely small outputs =E2=80=94 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 lim= it... and removing that limit obviously influenced BIP 444. So it appears t= o me that there is overwhelming majority support for limiting these types o= f "bulk dust" transactions, as they do present a legitimate conce= rn for node runners.

Concept

Flag a transaction as =E2=80=9Cbulk du= st=E2=80=9D if:

  • It has >=3D100 outputs each below a dynamically defined TinyTx thr= eshold, and
  • Those= tiny outputs make up >=3D60% of all outputs in the transaction.
  • When flagged, it would be considered nonstandard (relay policy) o= r invalid (if soft-forked into consensus).

    TinyTx threshold (dynamic halving schedule= )

    I originally considered a constant definition of what w= as 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 v= alid use-case of >100 outputs under 1,000 sats.

    Rather than f= ixing the =E2=80=9Ctiny=E2=80=9D threshold to a constant like 1,000 sats, t= he 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 2= 10,000 blocks (~4 years).
    • Never falls below 1 sat (hard floor).

    Year ---- B= lock Height -- TinyTx Threshold
    2028 ---= ~activation ---- 4096 sats
    2032 --- ~1,= 260,000 ---- 2048 sats
    2036 --- ~1,470,0= 00 ---- 1024 sats
    2040 --- ~1,680,000 --= -- 512 sats
    =E2=80=A6 -- every 210,000 b= locks -- =E2=80=A6 until 1 sat floor

    This gradual halving ensure= s the definition of "tiny" stays relevant as Bitcoin=E2=80=99s va= lue rises.
    For example, if 1 sat =3D $1 = someday, having 100 outputs worth <1,000 sats each would no longer repre= sent spam =E2=80=94 but rather normal payments.
    By then, the TinyTx limit would already have adjusted down automati= cally.

    Pat= terns this would limit

    • Fake pubkeys or scripts used to embed data via many UTX= Os
    • 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, numero= us outputs =E2=80=94 making them several times more costly under this rule.=

    Non-goals= / unaffected

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

    Why a ratio and a count?

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

    Inquiry

    • Are there credible, = non-spam use cases that truly require >=3D100 sub-4k-sat outputs (or equ= ivalent at later eras) and a >=3D60% 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

    Th= is proposal doesn=E2=80=99t censor any monetary transaction or prevent insc= riptions; it simply prices storage according to resource cost.
    It keeps the chain =E2=80=9Clight and nimble=E2=80= =9D for everyday payments while allowing future flexibility =E2=80=94 becau= se the TinyTx definition decreases automatically in line with halvings and = Bitcoin=E2=80=99s long-term value growth.

    CODE SKETCHES
    (with minimal syntax highlighting here:=C2=A0https://pastebi= n.com/9qdQCH83)

    RELAY POLICY FILTER sketch =E2=80=94

    // Place in src/policy/policy.cpp, and call from within Is= StandardTx() before returning: // if (IsBulkDust(tx, reason)) // return false; // reject as nonstandard // =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D bool IsBulkDust(const CTransaction& tx, std::string& reason) { static constexpr int MAX_TINY_OUTPUTS =3D 100; // >= =3D100 tiny outputs triggers ratio check static constexpr double TINY_RATIO_THRESHOLD =3D 0.6; // >= =3D60% of all outputs tiny =E2=86=92 reject static constexpr CAmount BASE_TINY_THRESHOLD =3D 4096; // starti= ng tiny threshold (sats) static constexpr int64_t FIRST_TINY_HALVING_H =3D 1260000; // first = halving of tiny threshold static constexpr int64_t HALVING_INTERVAL =3D 210000; // blocks= per subsequent halving static constexpr CAmount MIN_TINY_FLOOR =3D 1; // never = below 1 sat const int total =3D tx.vout.size(); if (total =3D=3D 0) return false; int currentHeight =3D chainActive.Tip() ? chainActive.Tip()->nHeight= : 0; // Era index for TinyTx threshold, anchored at FIRST_TINY_HALVING_H (no= t subsidy eras) int era =3D 0; if (currentHeight >=3D FIRST_TINY_HALVING_H) { era =3D 1 + static_cast<int>((currentHeight - FIRST_TINY_HALV= ING_H) / HALVING_INTERVAL); } CAmount tinyThresh =3D BASE_TINY_THRESHOLD >> era; // halve = per era if (tinyThresh < MIN_TINY_FLOOR) tinyThresh =3D MIN_TINY_FLOOR; int tiny =3D 0; for (const auto& out : tx.vout) { if (out.nValue < tinyThresh) ++tiny; } if (tiny >=3D MAX_TINY_OUTPUTS && (static_cast<double>= (tiny) / total) >=3D TINY_RATIO_THRESHOLD) { reason =3D 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; }

    CONSE= NSUS (soft-fork, hybrid activation) sketch =E2=80=94

    // Helpers in src/consensus/tx_c= heck.cpp; activation/enforcement in src/validation.cpp // Also define deployment in: src/consensus/params.h, src/chainparams.cpp, = src/versionbits.* // =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D // ----------------------------------------------------------------------- // --- In src/consensus/tx_check.cpp (helper only; no params needed) --- // ----------------------------------------------------------------------- static constexpr CAmount BASE_TINY_THRESHOLD =3D 4096; static constexpr int64_t FIRST_TINY_HALVING_H =3D 1260000; static constexpr int64_t HALVING_INTERVAL =3D 210000; static constexpr int MAX_TINY_OUTPUTS =3D 100; static constexpr double TINY_RATIO_THRESHOLD =3D 0.6; static constexpr CAmount MIN_TINY_FLOOR =3D 1; bool IsBulkDust(const CTransaction& tx, int currentHeight) // expose vi= a tx_check.h if needed { const int total =3D tx.vout.size(); if (total =3D=3D 0) return false; int era =3D 0; if (currentHeight >=3D FIRST_TINY_HALVING_H) { era =3D 1 + static_cast<int>((currentHeight - FIRST_TINY_HALV= ING_H) / HALVING_INTERVAL); } CAmount tinyThresh =3D BASE_TINY_THRESHOLD >> era; if (tinyThresh < MIN_TINY_FLOOR) tinyThresh =3D MIN_TINY_FLOOR; int tiny =3D 0; for (const auto& out : tx.vout) { if (out.nValue < tinyThresh) ++tiny; } if (tiny >=3D MAX_TINY_OUTPUTS && (static_cast<double>= (tiny) / total) >=3D 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 =3D chainparams.GetConsensus(); int currentHeight =3D chainActive.Tip() ? chainActive.Tip()->nHeight : 0= ; const bool bulk_dust_active =3D DeploymentActiveAtTip(params, Consensus::DEPLOYMENT_BULK_DUST_LIMIT) || (currentHeight >=3D params.BulkDustActivationHeight); if (bulk_dust_active) { if (IsBulkDust(tx, currentHeight)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "too-ma= ny-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 =3D 12; consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nStartTime = =3D 1767225600; // 2026-01-01 UTC consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].nTimeout = =3D 1838160000; // 2028-04-01 UTC consensus.vDeployments[Consensus::DEPLOYMENT_BULK_DUST_LIMIT].min_activatio= n_height =3D 969696; consensus.BulkDustActivationHeight =3D 1021021; // flag-day fallback=

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