Skip to content

Commit

Permalink
Merge bitcoin/bitcoin#30239: Ephemeral Dust
Browse files Browse the repository at this point in the history
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
  • Loading branch information
glozow committed Nov 13, 2024
2 parents 1dda189 + 5c2e291 commit b0222bb
Show file tree
Hide file tree
Showing 20 changed files with 1,218 additions and 3 deletions.
12 changes: 12 additions & 0 deletions doc/release-notes-30239.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
P2P and network changes
-----------------------

Ephemeral dust is a new concept that allows a single
dust output in a transaction, provided the transaction
is zero fee. In order to spend any unconfirmed outputs
from this transaction, the spender must also spend
this dust in addition to any other outputs.

In other words, this type of transaction
should be created in a transaction package where
the dust is both created and spent simultaneously.
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL
node/utxo_snapshot.cpp
node/warnings.cpp
noui.cpp
policy/ephemeral_policy.cpp
policy/fees.cpp
policy/fees_args.cpp
policy/packages.cpp
Expand Down
1 change: 1 addition & 0 deletions src/bench/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ add_executable(bench_bitcoin
load_external.cpp
lockedpool.cpp
logging.cpp
mempool_ephemeral_spends.cpp
mempool_eviction.cpp
mempool_stress.cpp
merkle_root.cpp
Expand Down
83 changes: 83 additions & 0 deletions src/bench/mempool_ephemeral_spends.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2011-2022 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <bench/bench.h>
#include <consensus/amount.h>
#include <kernel/cs_main.h>
#include <policy/ephemeral_policy.h>
#include <policy/policy.h>
#include <primitives/transaction.h>
#include <script/script.h>
#include <sync.h>
#include <test/util/setup_common.h>
#include <txmempool.h>
#include <util/check.h>

#include <cstdint>
#include <memory>
#include <vector>


static void AddTx(const CTransactionRef& tx, CTxMemPool& pool) EXCLUSIVE_LOCKS_REQUIRED(cs_main, pool.cs)
{
int64_t nTime{0};
unsigned int nHeight{1};
uint64_t sequence{0};
bool spendsCoinbase{false};
unsigned int sigOpCost{4};
uint64_t fee{0};
LockPoints lp;
pool.addUnchecked(CTxMemPoolEntry(
tx, fee, nTime, nHeight, sequence,
spendsCoinbase, sigOpCost, lp));
}

static void MempoolCheckEphemeralSpends(benchmark::Bench& bench)
{
const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();

int number_outputs{1000};
if (bench.complexityN() > 1) {
number_outputs = static_cast<int>(bench.complexityN());
}

// Tx with many outputs
CMutableTransaction tx1 = CMutableTransaction();
tx1.vin.resize(1);
tx1.vout.resize(number_outputs);
for (size_t i = 0; i < tx1.vout.size(); i++) {
tx1.vout[i].scriptPubKey = CScript();
// Each output progressively larger
tx1.vout[i].nValue = i * CENT;
}

const auto& parent_txid = tx1.GetHash();

// Spends all outputs of tx1, other details don't matter
CMutableTransaction tx2 = CMutableTransaction();
tx2.vin.resize(tx1.vout.size());
for (size_t i = 0; i < tx2.vin.size(); i++) {
tx2.vin[0].prevout.hash = parent_txid;
tx2.vin[0].prevout.n = i;
}
tx2.vout.resize(1);

CTxMemPool& pool = *Assert(testing_setup->m_node.mempool);
LOCK2(cs_main, pool.cs);
// Create transaction references outside the "hot loop"
const CTransactionRef tx1_r{MakeTransactionRef(tx1)};
const CTransactionRef tx2_r{MakeTransactionRef(tx2)};

AddTx(tx1_r, pool);

uint32_t iteration{0};

bench.run([&]() NO_THREAD_SAFETY_ANALYSIS {

CheckEphemeralSpends({tx2_r}, /*dust_relay_rate=*/CFeeRate(iteration * COIN / 10), pool);
iteration++;
});
}

BENCHMARK(MempoolCheckEphemeralSpends, benchmark::PriorityLevel::HIGH);
1 change: 1 addition & 0 deletions src/kernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ add_library(bitcoinkernel
../node/blockstorage.cpp
../node/chainstate.cpp
../node/utxo_snapshot.cpp
../policy/ephemeral_policy.cpp
../policy/feerate.cpp
../policy/packages.cpp
../policy/policy.cpp
Expand Down
78 changes: 78 additions & 0 deletions src/policy/ephemeral_policy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) 2024-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <policy/ephemeral_policy.h>
#include <policy/policy.h>

bool HasDust(const CTransactionRef& tx, CFeeRate dust_relay_rate)
{
return std::any_of(tx->vout.cbegin(), tx->vout.cend(), [&](const auto& output) { return IsDust(output, dust_relay_rate); });
}

bool CheckValidEphemeralTx(const CTransactionRef& tx, CFeeRate dust_relay_rate, CAmount base_fee, CAmount mod_fee, TxValidationState& state)
{
// We never want to give incentives to mine this transaction alone
if ((base_fee != 0 || mod_fee != 0) && HasDust(tx, dust_relay_rate)) {
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "dust", "tx with dust output must be 0-fee");
}

return true;
}

std::optional<Txid> CheckEphemeralSpends(const Package& package, CFeeRate dust_relay_rate, const CTxMemPool& tx_pool)
{
if (!Assume(std::all_of(package.cbegin(), package.cend(), [](const auto& tx){return tx != nullptr;}))) {
// Bail out of spend checks if caller gave us an invalid package
return std::nullopt;
}

std::map<Txid, CTransactionRef> map_txid_ref;
for (const auto& tx : package) {
map_txid_ref[tx->GetHash()] = tx;
}

for (const auto& tx : package) {
Txid txid = tx->GetHash();
std::unordered_set<Txid, SaltedTxidHasher> processed_parent_set;
std::unordered_set<COutPoint, SaltedOutpointHasher> unspent_parent_dust;

for (const auto& tx_input : tx->vin) {
const Txid& parent_txid{tx_input.prevout.hash};
// Skip parents we've already checked dust for
if (processed_parent_set.contains(parent_txid)) continue;

// We look for an in-package or in-mempool dependency
CTransactionRef parent_ref = nullptr;
if (auto it = map_txid_ref.find(parent_txid); it != map_txid_ref.end()) {
parent_ref = it->second;
} else {
parent_ref = tx_pool.get(parent_txid);
}

// Check for dust on parents
if (parent_ref) {
for (uint32_t out_index = 0; out_index < parent_ref->vout.size(); out_index++) {
const auto& tx_output = parent_ref->vout[out_index];
if (IsDust(tx_output, dust_relay_rate)) {
unspent_parent_dust.insert(COutPoint(parent_txid, out_index));
}
}
}

processed_parent_set.insert(parent_txid);
}

// Now that we have gathered parents' dust, make sure it's spent
// by the child
for (const auto& tx_input : tx->vin) {
unspent_parent_dust.erase(tx_input.prevout);
}

if (!unspent_parent_dust.empty()) {
return txid;
}
}

return std::nullopt;
}
55 changes: 55 additions & 0 deletions src/policy/ephemeral_policy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2024-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_POLICY_EPHEMERAL_POLICY_H
#define BITCOIN_POLICY_EPHEMERAL_POLICY_H

#include <policy/packages.h>
#include <policy/policy.h>
#include <primitives/transaction.h>
#include <txmempool.h>

/** These utility functions ensure that ephemeral dust is safely
* created and spent without unduly risking them entering the utxo
* set.
* This is ensured by requiring:
* - CheckValidEphemeralTx checks are respected
* - The parent has no child (and 0-fee as implied above to disincentivize mining)
* - OR the parent transaction has exactly one child, and the dust is spent by that child
*
* Imagine three transactions:
* TxA, 0-fee with two outputs, one non-dust, one dust
* TxB, spends TxA's non-dust
* TxC, spends TxA's dust
*
* All the dust is spent if TxA+TxB+TxC is accepted, but the mining template may just pick
* up TxA+TxB rather than the three "legal configurations:
* 1) None
* 2) TxA+TxB+TxC
* 3) TxA+TxC
* By requiring the child transaction to sweep any dust from the parent txn, we ensure that
* there is a single child only, and this child, or the child's descendants,
* are the only way to bring fees.
*/

/** Returns true if transaction contains dust */
bool HasDust(const CTransactionRef& tx, CFeeRate dust_relay_rate);

/* All the following checks are only called if standardness rules are being applied. */

/** Must be called for each transaction once transaction fees are known.
* Does context-less checks about a single transaction.
* Returns false if the fee is non-zero and dust exists, populating state. True otherwise.
*/
bool CheckValidEphemeralTx(const CTransactionRef& tx, CFeeRate dust_relay_rate, CAmount base_fee, CAmount mod_fee, TxValidationState& state);

/** Must be called for each transaction(package) if any dust is in the package.
* Checks that each transaction's parents have their dust spent by the child,
* where parents are either in the mempool or in the package itself.
* The function returns std::nullopt if all dust is properly spent, or the txid of the violating child spend.
*/
std::optional<Txid> CheckEphemeralSpends(const Package& package, CFeeRate dust_relay_rate, const CTxMemPool& tx_pool);

#endif // BITCOIN_POLICY_EPHEMERAL_POLICY_H
10 changes: 8 additions & 2 deletions src/policy/policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_dat
}

unsigned int nDataOut = 0;
unsigned int num_dust_outputs{0};
TxoutType whichType;
for (const CTxOut& txout : tx.vout) {
if (!::IsStandard(txout.scriptPubKey, max_datacarrier_bytes, whichType)) {
Expand All @@ -142,11 +143,16 @@ bool IsStandardTx(const CTransaction& tx, const std::optional<unsigned>& max_dat
reason = "bare-multisig";
return false;
} else if (IsDust(txout, dust_relay_fee)) {
reason = "dust";
return false;
num_dust_outputs++;
}
}

// Only MAX_DUST_OUTPUTS_PER_TX dust is permitted(on otherwise valid ephemeral dust)
if (num_dust_outputs > MAX_DUST_OUTPUTS_PER_TX) {
reason = "dust";
return false;
}

// only one OP_RETURN txout is permitted
if (nDataOut > 1) {
reason = "multi-op-return";
Expand Down
4 changes: 4 additions & 0 deletions src/policy/policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ static const unsigned int MAX_OP_RETURN_RELAY = 83;
*/
static constexpr unsigned int EXTRA_DESCENDANT_TX_SIZE_LIMIT{10000};

/**
* Maximum number of ephemeral dust outputs allowed.
*/
static constexpr unsigned int MAX_DUST_OUTPUTS_PER_TX{1};

/**
* Mandatory script verification flags that all new transactions must comply with for
Expand Down
11 changes: 10 additions & 1 deletion src/rpc/mining.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <node/context.h>
#include <node/miner.h>
#include <node/warnings.h>
#include <policy/ephemeral_policy.h>
#include <pow.h>
#include <rpc/blockchain.h>
#include <rpc/mining.h>
Expand Down Expand Up @@ -491,7 +492,15 @@ static RPCHelpMan prioritisetransaction()
throw JSONRPCError(RPC_INVALID_PARAMETER, "Priority is no longer supported, dummy argument to prioritisetransaction must be 0.");
}

EnsureAnyMemPool(request.context).PrioritiseTransaction(hash, nAmount);
CTxMemPool& mempool = EnsureAnyMemPool(request.context);

// Non-0 fee dust transactions are not allowed for entry, and modification not allowed afterwards
const auto& tx = mempool.get(hash);
if (tx && HasDust(tx, mempool.m_opts.dust_relay_feerate)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Priority is not supported for transactions with dust outputs.");
}

mempool.PrioritiseTransaction(hash, nAmount);
return true;
},
};
Expand Down
Loading

0 comments on commit b0222bb

Please sign in to comment.