Skip to content

Commit e1d3e81

Browse files
committed
policy: Allow dust in transactions, spent in-mempool
Also known as Ephemeral Dust. We try to ensure that dust is spent in blocks by requiring: - ephemeral dust tx is 0-fee - ephemeral dust tx only has one dust output - If the ephemeral dust transaction has a child, the dust is spent by by that child. 0-fee requirement means there is no incentive to mine a transaction which doesn't have a child bringing its own fees for the transaction package.
1 parent 04b2714 commit e1d3e81

File tree

8 files changed

+187
-2
lines changed

8 files changed

+187
-2
lines changed

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL
252252
node/utxo_snapshot.cpp
253253
node/warnings.cpp
254254
noui.cpp
255+
policy/ephemeral_policy.cpp
255256
policy/fees.cpp
256257
policy/fees_args.cpp
257258
policy/packages.cpp

src/kernel/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ add_library(bitcoinkernel
3333
../node/blockstorage.cpp
3434
../node/chainstate.cpp
3535
../node/utxo_snapshot.cpp
36+
../policy/ephemeral_policy.cpp
3637
../policy/feerate.cpp
3738
../policy/packages.cpp
3839
../policy/policy.cpp

src/policy/ephemeral_policy.cpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2024-present The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <policy/ephemeral_policy.h>
6+
#include <policy/policy.h>
7+
8+
bool HasDust(const CTransactionRef& tx, CFeeRate dust_relay_rate)
9+
{
10+
return std::any_of(tx->vout.cbegin(), tx->vout.cend(), [&](const auto& output) { return IsDust(output, dust_relay_rate); });
11+
}
12+
13+
bool CheckValidEphemeralTx(const CTransactionRef& tx, CFeeRate dust_relay_rate, CAmount base_fee, CAmount mod_fee, TxValidationState& state)
14+
{
15+
// We never want to give incentives to mine this transaction alone
16+
if ((base_fee != 0 || mod_fee != 0) && HasDust(tx, dust_relay_rate)) {
17+
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "dust", "tx with dust output must be 0-fee");
18+
}
19+
20+
return true;
21+
}
22+
23+
std::optional<Txid> CheckEphemeralSpends(const Package& package, CFeeRate dust_relay_rate, const CTxMemPool& tx_pool)
24+
{
25+
if (!Assume(std::all_of(package.cbegin(), package.cend(), [](const auto& tx){return tx != nullptr;}))) {
26+
// Bail out of spend checks if caller gave us an invalid package
27+
return std::nullopt;
28+
}
29+
30+
std::map<Txid, CTransactionRef> map_txid_ref;
31+
for (const auto& tx : package) {
32+
map_txid_ref[tx->GetHash()] = tx;
33+
}
34+
35+
for (const auto& tx : package) {
36+
Txid txid = tx->GetHash();
37+
std::unordered_set<Txid, SaltedTxidHasher> processed_parent_set;
38+
std::unordered_set<COutPoint, SaltedOutpointHasher> unspent_parent_dust;
39+
40+
for (const auto& tx_input : tx->vin) {
41+
const Txid& parent_txid{tx_input.prevout.hash};
42+
// Skip parents we've already checked dust for
43+
if (processed_parent_set.contains(parent_txid)) continue;
44+
45+
// We look for an in-package or in-mempool dependency
46+
CTransactionRef parent_ref = nullptr;
47+
if (auto it = map_txid_ref.find(parent_txid); it != map_txid_ref.end()) {
48+
parent_ref = it->second;
49+
} else {
50+
parent_ref = tx_pool.get(parent_txid);
51+
}
52+
53+
// Check for dust on parents
54+
if (parent_ref) {
55+
for (uint32_t out_index = 0; out_index < parent_ref->vout.size(); out_index++) {
56+
const auto& tx_output = parent_ref->vout[out_index];
57+
if (IsDust(tx_output, dust_relay_rate)) {
58+
unspent_parent_dust.insert(COutPoint(parent_txid, out_index));
59+
}
60+
}
61+
}
62+
63+
processed_parent_set.insert(parent_txid);
64+
}
65+
66+
// Now that we have gathered parents' dust, make sure it's spent
67+
// by the child
68+
for (const auto& tx_input : tx->vin) {
69+
unspent_parent_dust.erase(tx_input.prevout);
70+
}
71+
72+
if (!unspent_parent_dust.empty()) {
73+
return txid;
74+
}
75+
}
76+
77+
return std::nullopt;
78+
}

src/policy/ephemeral_policy.h

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) 2024-present The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_POLICY_EPHEMERAL_POLICY_H
6+
#define BITCOIN_POLICY_EPHEMERAL_POLICY_H
7+
8+
#include <policy/packages.h>
9+
#include <policy/policy.h>
10+
#include <primitives/transaction.h>
11+
#include <txmempool.h>
12+
13+
/** These utility functions ensure that ephemeral dust is safely
14+
* created and spent without unduly risking them entering the utxo
15+
* set.
16+
17+
* This is ensured by requiring:
18+
* - CheckValidEphemeralTx checks are respected
19+
* - The parent has no child (and 0-fee as implied above to disincentivize mining)
20+
* - OR the parent transaction has exactly one child, and the dust is spent by that child
21+
*
22+
* Imagine three transactions:
23+
* TxA, 0-fee with two outputs, one non-dust, one dust
24+
* TxB, spends TxA's non-dust
25+
* TxC, spends TxA's dust
26+
*
27+
* All the dust is spent if TxA+TxB+TxC is accepted, but the mining template may just pick
28+
* up TxA+TxB rather than the three "legal configurations:
29+
* 1) None
30+
* 2) TxA+TxB+TxC
31+
* 3) TxA+TxC
32+
* By requiring the child transaction to sweep any dust from the parent txn, we ensure that
33+
* there is a single child only, and this child, or the child's descendants,
34+
* are the only way to bring fees.
35+
*/
36+
37+
/** Returns true if transaction contains dust */
38+
bool HasDust(const CTransactionRef& tx, CFeeRate dust_relay_rate);
39+
40+
/* All the following checks are only called if standardness rules are being applied. */
41+
42+
/** Must be called for each transaction once transaction fees are known.
43+
* Does context-less checks about a single transaction.
44+
* Returns false if the fee is non-zero and dust exists, populating state. True otherwise.
45+
*/
46+
bool CheckValidEphemeralTx(const CTransactionRef& tx, CFeeRate dust_relay_rate, CAmount base_fee, CAmount mod_fee, TxValidationState& state);
47+
48+
/** Must be called for each transaction(package) if any dust is in the package.
49+
* Checks that each transaction's parents have their dust spent by the child,
50+
* where parents are either in the mempool or in the package itself.
51+
* The function returns std::nullopt if all dust is properly spent, or the txid of the violating child spend.
52+
*/
53+
std::optional<Txid> CheckEphemeralSpends(const Package& package, CFeeRate dust_relay_rate, const CTxMemPool& tx_pool);
54+
55+
#endif // BITCOIN_POLICY_EPHEMERAL_POLICY_H

src/policy/policy.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_dat
129129
}
130130

131131
unsigned int nDataOut = 0;
132+
unsigned int num_dust_outputs{0};
132133
TxoutType whichType;
133134
for (const CTxOut& txout : tx.vout) {
134135
if (!::IsStandard(txout.scriptPubKey, max_datacarrier_bytes, whichType)) {
@@ -142,11 +143,16 @@ bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_dat
142143
reason = "bare-multisig";
143144
return false;
144145
} else if (IsDust(txout, dust_relay_fee)) {
145-
reason = "dust";
146-
return false;
146+
num_dust_outputs++;
147147
}
148148
}
149149

150+
// Only MAX_DUST_OUTPUTS_PER_TX dust is permitted(on otherwise valid ephemeral dust)
151+
if (num_dust_outputs > MAX_DUST_OUTPUTS_PER_TX) {
152+
reason = "dust";
153+
return false;
154+
}
155+
150156
// only one OP_RETURN txout is permitted
151157
if (nDataOut > 1) {
152158
reason = "multi-op-return";

src/policy/policy.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ static const unsigned int MAX_OP_RETURN_RELAY = 83;
7777
*/
7878
static constexpr unsigned int EXTRA_DESCENDANT_TX_SIZE_LIMIT{10000};
7979

80+
/**
81+
* Maximum number of ephemeral dust outputs allowed.
82+
*/
83+
static constexpr unsigned int MAX_DUST_OUTPUTS_PER_TX{1};
8084

8185
/**
8286
* Mandatory script verification flags that all new transactions must comply with for

src/test/transaction_tests.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,11 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
813813
// Check dust with default relay fee:
814814
CAmount nDustThreshold = 182 * g_dust.GetFeePerK() / 1000;
815815
BOOST_CHECK_EQUAL(nDustThreshold, 546);
816+
817+
// Add dust output to take dust slot, still standard!
818+
t.vout.emplace_back(0, t.vout[0].scriptPubKey);
819+
CheckIsStandard(t);
820+
816821
// dust:
817822
t.vout[0].nValue = nDustThreshold - 1;
818823
CheckIsNotStandard(t, "dust");
@@ -969,6 +974,10 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
969974
CheckIsNotStandard(t, "bare-multisig");
970975
g_bare_multi = DEFAULT_PERMIT_BAREMULTISIG;
971976

977+
// Add dust output to take dust slot
978+
assert(t.vout.size() == 1);
979+
t.vout.emplace_back(0, t.vout[0].scriptPubKey);
980+
972981
// Check compressed P2PK outputs dust threshold (must have leading 02 or 03)
973982
t.vout[0].scriptPubKey = CScript() << std::vector<unsigned char>(33, 0x02) << OP_CHECKSIG;
974983
t.vout[0].nValue = 576;

src/validation.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <logging/timer.h>
3333
#include <node/blockstorage.h>
3434
#include <node/utxo_snapshot.h>
35+
#include <policy/ephemeral_policy.h>
3536
#include <policy/policy.h>
3637
#include <policy/rbf.h>
3738
#include <policy/settings.h>
@@ -912,6 +913,13 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
912913
fSpendsCoinbase, nSigOpsCost, lock_points.value()));
913914
ws.m_vsize = entry->GetTxSize();
914915

916+
// Enforces 0-fee for dust transactions, no incentive to be mined alone
917+
if (m_pool.m_opts.require_standard) {
918+
if (!CheckValidEphemeralTx(ptx, m_pool.m_opts.dust_relay_feerate, ws.m_base_fees, ws.m_modified_fees, state)) {
919+
return false; // state filled in by CheckValidEphemeralTx
920+
}
921+
}
922+
915923
if (nSigOpsCost > MAX_STANDARD_TX_SIGOPS_COST)
916924
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "bad-txns-too-many-sigops",
917925
strprintf("%d", nSigOpsCost));
@@ -1432,6 +1440,16 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
14321440
return MempoolAcceptResult::Failure(ws.m_state);
14331441
}
14341442

1443+
if (m_pool.m_opts.require_standard) {
1444+
if (const auto ephemeral_violation{CheckEphemeralSpends(/*package=*/{ptx}, m_pool.m_opts.dust_relay_feerate, m_pool)}) {
1445+
const Txid& txid = ephemeral_violation.value();
1446+
Assume(txid == ptx->GetHash());
1447+
ws.m_state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "missing-ephemeral-spends",
1448+
strprintf("tx %s did not spend parent's ephemeral dust", txid.ToString()));
1449+
return MempoolAcceptResult::Failure(ws.m_state);
1450+
}
1451+
}
1452+
14351453
if (m_subpackage.m_rbf && !ReplacementChecks(ws)) {
14361454
if (ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE) {
14371455
// Failed for incentives-based fee reasons. Provide the effective feerate and which tx was included.
@@ -1570,6 +1588,19 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
15701588
return PackageMempoolAcceptResult(package_state, std::move(results));
15711589
}
15721590

1591+
// Now that we've bounded the resulting possible ancestry count, check package for dust spends
1592+
if (m_pool.m_opts.require_standard) {
1593+
if (const auto ephemeral_violation{CheckEphemeralSpends(txns, m_pool.m_opts.dust_relay_feerate, m_pool)}) {
1594+
const Txid& child_txid = ephemeral_violation.value();
1595+
TxValidationState child_state;
1596+
child_state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "missing-ephemeral-spends",
1597+
strprintf("tx %s did not spend parent's ephemeral dust", child_txid.ToString()));
1598+
package_state.Invalid(PackageValidationResult::PCKG_TX, "unspent-dust");
1599+
results.emplace(child_txid, MempoolAcceptResult::Failure(child_state));
1600+
return PackageMempoolAcceptResult(package_state, std::move(results));
1601+
}
1602+
}
1603+
15731604
for (Workspace& ws : workspaces) {
15741605
ws.m_package_feerate = package_feerate;
15751606
if (!PolicyScriptChecks(args, ws)) {

0 commit comments

Comments
 (0)