Skip to content

Commit b0222bb

Browse files
committed
Merge bitcoin/bitcoin#30239: Ephemeral Dust
5c2e291 bench: Add basic CheckEphemeralSpends benchmark (Greg Sanders) 3f6559f Add release note for ephemeral dust (Greg Sanders) 71a6ab4 test: unit test for CheckEphemeralSpends (Greg Sanders) 21d28b2 fuzz: add ephemeral_package_eval harness (Greg Sanders) 127719f test: Add CheckMempoolEphemeralInvariants (Greg Sanders) e2e30e8 functional test: Add ephemeral dust tests (Greg Sanders) 4e68f90 rpc: disallow in-mempool prioritisation of dusty tx (Greg Sanders) e1d3e81 policy: Allow dust in transactions, spent in-mempool (Greg Sanders) 04b2714 functional test: Add new -dustrelayfee=0 test case (Greg Sanders) Pull request description: A replacement for bitcoin/bitcoin#29001 Now that we have 1P1C relay, TRUC transactions and sibling eviction, it makes sense to retarget this feature more narrowly by not introducing a new output type, and simple focusing on the feature of allowing temporary dust in the mempool. Users of this can immediately use dust outputs as: 1. Single keyed anchor (can be shared by multiple parties) 2. Single unkeyed anchor, ala P2A Which is useful when the parent transaction cannot have fees for technical or accounting reasons. What I'm calling "keyed" anchors would be used anytime you don't want a third party to be able to run off with the utxo. As a motivating example, in Ark there is the concept of a "forfeit transaction" which spends a "connector output". The connector output would ideally be 0-value, but you would not want that utxo spend by anyone, because this would cause financial loss for the coordinator of the service: https://arkdev.info/docs/learn/concepts#forfeit-transaction Note that this specific use-case likely doesn't work as it involves a tree of dust, but the connector idea in general demonstrates how it could be used. Another related example is connector outputs in BitVM2: https://bitvm.org/bitvm2.html . Note that non-TRUC usage will be impractical unless the minrelay requirement on individual transactions are dropped in general, which should happen post-cluster mempool. Lightning Network intends to use this feature post-29.0 if available: lightning/bolts#1171 (comment) It's also useful for Ark, ln-symmetry, spacechains, Timeout Trees, and other constructs with large presigned trees or other large-N party smart contracts. ACKs for top commit: glozow: reACK 5c2e291 via range-diff. Nothing but a rebase and removing the conflict. theStack: re-ACK 5c2e291 Tree-SHA512: 88e6a6b3b91dc425de47ccd68b7668c8e98c5683712e892c588f79ad639ae95c665e2d5563dd5e5797983e7542cbd1d4353bc90a7298d45a1843b05a417f09f5
2 parents 1dda189 + 5c2e291 commit b0222bb

20 files changed

+1218
-3
lines changed

doc/release-notes-30239.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
P2P and network changes
2+
-----------------------
3+
4+
Ephemeral dust is a new concept that allows a single
5+
dust output in a transaction, provided the transaction
6+
is zero fee. In order to spend any unconfirmed outputs
7+
from this transaction, the spender must also spend
8+
this dust in addition to any other outputs.
9+
10+
In other words, this type of transaction
11+
should be created in a transaction package where
12+
the dust is both created and spent simultaneously.

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/bench/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ add_executable(bench_bitcoin
3434
load_external.cpp
3535
lockedpool.cpp
3636
logging.cpp
37+
mempool_ephemeral_spends.cpp
3738
mempool_eviction.cpp
3839
mempool_stress.cpp
3940
merkle_root.cpp
Lines changed: 83 additions & 0 deletions
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/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/rpc/mining.cpp

Lines changed: 10 additions & 1 deletion
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>
@@ -491,7 +492,15 @@ static RPCHelpMan prioritisetransaction()
491492
throw JSONRPCError(RPC_INVALID_PARAMETER, "Priority is no longer supported, dummy argument to prioritisetransaction must be 0.");
492493
}
493494

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

0 commit comments

Comments
 (0)