Skip to content

Commit 7c4322c

Browse files
authored
Merge pull request #69 from instagibbs/2024-11-eph_dust_inq_backport
Ephemeral Dust
2 parents a4ccaf6 + efa9ff7 commit 7c4322c

18 files changed

+1209
-3
lines changed

src/Makefile.am

+3
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ BITCOIN_CORE_H = \
243243
node/warnings.h \
244244
noui.h \
245245
outputtype.h \
246+
policy/ephemeral_policy.h \
246247
policy/feerate.h \
247248
policy/fees.h \
248249
policy/fees_args.h \
@@ -445,6 +446,7 @@ libbitcoin_node_a_SOURCES = \
445446
node/utxo_snapshot.cpp \
446447
node/warnings.cpp \
447448
noui.cpp \
449+
policy/ephemeral_policy.cpp \
448450
policy/fees.cpp \
449451
policy/fees_args.cpp \
450452
policy/packages.cpp \
@@ -951,6 +953,7 @@ libbitcoinkernel_la_SOURCES = \
951953
node/blockstorage.cpp \
952954
node/chainstate.cpp \
953955
node/utxo_snapshot.cpp \
956+
policy/ephemeral_policy.cpp \
954957
policy/feerate.cpp \
955958
policy/packages.cpp \
956959
policy/policy.cpp \

src/Makefile.bench.include

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ bench_bench_bitcoin_SOURCES = \
4040
bench/load_external.cpp \
4141
bench/lockedpool.cpp \
4242
bench/logging.cpp \
43+
bench/mempool_ephemeral_spends.cpp \
4344
bench/mempool_eviction.cpp \
4445
bench/mempool_stress.cpp \
4546
bench/merkle_root.cpp \
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) 2011-2022 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 <bench/bench.h>
6+
#include <consensus/amount.h>
7+
#include <kernel/cs_main.h>
8+
#include <policy/ephemeral_policy.h>
9+
#include <policy/policy.h>
10+
#include <primitives/transaction.h>
11+
#include <script/script.h>
12+
#include <sync.h>
13+
#include <test/util/setup_common.h>
14+
#include <txmempool.h>
15+
#include <util/check.h>
16+
17+
#include <cstdint>
18+
#include <memory>
19+
#include <vector>
20+
21+
22+
static void AddTx(const CTransactionRef& tx, CTxMemPool& pool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, pool.cs)
23+
{
24+
int64_t nTime{0};
25+
unsigned int nHeight{1};
26+
uint64_t sequence{0};
27+
bool spendsCoinbase{false};
28+
unsigned int sigOpCost{4};
29+
uint64_t fee{0};
30+
LockPoints lp;
31+
pool.addUnchecked(CTxMemPoolEntry(
32+
tx, fee, nTime, nHeight, sequence,
33+
spendsCoinbase, sigOpCost, lp));
34+
}
35+
36+
static void MempoolCheckEphemeralSpends(benchmark::Bench& bench)
37+
{
38+
const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
39+
40+
int number_outputs{1000};
41+
if (bench.complexityN() > 1) {
42+
number_outputs = static_cast<int>(bench.complexityN());
43+
}
44+
45+
// Tx with many outputs
46+
CMutableTransaction tx1 = CMutableTransaction();
47+
tx1.vin.resize(1);
48+
tx1.vout.resize(number_outputs);
49+
for (size_t i = 0; i < tx1.vout.size(); i++) {
50+
tx1.vout[i].scriptPubKey = CScript();
51+
// Each output progressively larger
52+
tx1.vout[i].nValue = i * CENT;
53+
}
54+
55+
const auto& parent_txid = tx1.GetHash();
56+
57+
// Spends all outputs of tx1, other details don't matter
58+
CMutableTransaction tx2 = CMutableTransaction();
59+
tx2.vin.resize(tx1.vout.size());
60+
for (size_t i = 0; i < tx2.vin.size(); i++) {
61+
tx2.vin[0].prevout.hash = parent_txid;
62+
tx2.vin[0].prevout.n = i;
63+
}
64+
tx2.vout.resize(1);
65+
66+
CTxMemPool& pool = *Assert(testing_setup->m_node.mempool);
67+
LOCK2(cs_main, pool.cs);
68+
// Create transaction references outside the "hot loop"
69+
const CTransactionRef tx1_r{MakeTransactionRef(tx1)};
70+
const CTransactionRef tx2_r{MakeTransactionRef(tx2)};
71+
72+
AddTx(tx1_r, pool);
73+
74+
uint32_t iteration{0};
75+
76+
bench.run([&]() NO_THREAD_SAFETY_ANALYSIS {
77+
78+
CheckEphemeralSpends({tx2_r}, /*dust_relay_rate=*/CFeeRate(iteration * COIN / 10), pool);
79+
iteration++;
80+
});
81+
}
82+
83+
BENCHMARK(MempoolCheckEphemeralSpends, benchmark::PriorityLevel::HIGH);

src/policy/ephemeral_policy.cpp

+78
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

+55
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

+8-2
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

+4
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/rpc/mining.cpp

+10-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <node/context.h>
2424
#include <node/miner.h>
2525
#include <node/warnings.h>
26+
#include <policy/ephemeral_policy.h>
2627
#include <pow.h>
2728
#include <rpc/blockchain.h>
2829
#include <rpc/mining.h>
@@ -492,7 +493,15 @@ static RPCHelpMan prioritisetransaction()
492493
throw JSONRPCError(RPC_INVALID_PARAMETER, "Priority is no longer supported, dummy argument to prioritisetransaction must be 0.");
493494
}
494495

495-
EnsureAnyMemPool(request.context).PrioritiseTransaction(hash, nAmount);
496+
CTxMemPool& mempool = EnsureAnyMemPool(request.context);
497+
498+
// Non-0 fee dust transactions are not allowed for entry, and modification not allowed afterwards
499+
const auto& tx = mempool.get(hash);
500+
if (tx && HasDust(tx, mempool.m_opts.dust_relay_feerate)) {
501+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Priority is not supported for transactions with dust outputs.");
502+
}
503+
504+
mempool.PrioritiseTransaction(hash, nAmount);
496505
return true;
497506
},
498507
};

0 commit comments

Comments
 (0)