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.
This commit is contained in:
Greg Sanders 2024-07-19 12:25:23 -04:00
parent 04b2714fbb
commit e1d3e81ab4
8 changed files with 187 additions and 2 deletions

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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

View file

@ -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)) {
@ -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";

View file

@ -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

View file

@ -813,6 +813,11 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
// Check dust with default relay fee:
CAmount nDustThreshold = 182 * g_dust.GetFeePerK() / 1000;
BOOST_CHECK_EQUAL(nDustThreshold, 546);
// Add dust output to take dust slot, still standard!
t.vout.emplace_back(0, t.vout[0].scriptPubKey);
CheckIsStandard(t);
// dust:
t.vout[0].nValue = nDustThreshold - 1;
CheckIsNotStandard(t, "dust");
@ -969,6 +974,10 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
CheckIsNotStandard(t, "bare-multisig");
g_bare_multi = DEFAULT_PERMIT_BAREMULTISIG;
// Add dust output to take dust slot
assert(t.vout.size() == 1);
t.vout.emplace_back(0, t.vout[0].scriptPubKey);
// Check compressed P2PK outputs dust threshold (must have leading 02 or 03)
t.vout[0].scriptPubKey = CScript() << std::vector<unsigned char>(33, 0x02) << OP_CHECKSIG;
t.vout[0].nValue = 576;

View file

@ -32,6 +32,7 @@
#include <logging/timer.h>
#include <node/blockstorage.h>
#include <node/utxo_snapshot.h>
#include <policy/ephemeral_policy.h>
#include <policy/policy.h>
#include <policy/rbf.h>
#include <policy/settings.h>
@ -912,6 +913,13 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
fSpendsCoinbase, nSigOpsCost, lock_points.value()));
ws.m_vsize = entry->GetTxSize();
// Enforces 0-fee for dust transactions, no incentive to be mined alone
if (m_pool.m_opts.require_standard) {
if (!CheckValidEphemeralTx(ptx, m_pool.m_opts.dust_relay_feerate, ws.m_base_fees, ws.m_modified_fees, state)) {
return false; // state filled in by CheckValidEphemeralTx
}
}
if (nSigOpsCost > MAX_STANDARD_TX_SIGOPS_COST)
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "bad-txns-too-many-sigops",
strprintf("%d", nSigOpsCost));
@ -1432,6 +1440,16 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
return MempoolAcceptResult::Failure(ws.m_state);
}
if (m_pool.m_opts.require_standard) {
if (const auto ephemeral_violation{CheckEphemeralSpends(/*package=*/{ptx}, m_pool.m_opts.dust_relay_feerate, m_pool)}) {
const Txid& txid = ephemeral_violation.value();
Assume(txid == ptx->GetHash());
ws.m_state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "missing-ephemeral-spends",
strprintf("tx %s did not spend parent's ephemeral dust", txid.ToString()));
return MempoolAcceptResult::Failure(ws.m_state);
}
}
if (m_subpackage.m_rbf && !ReplacementChecks(ws)) {
if (ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE) {
// Failed for incentives-based fee reasons. Provide the effective feerate and which tx was included.
@ -1570,6 +1588,19 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
return PackageMempoolAcceptResult(package_state, std::move(results));
}
// Now that we've bounded the resulting possible ancestry count, check package for dust spends
if (m_pool.m_opts.require_standard) {
if (const auto ephemeral_violation{CheckEphemeralSpends(txns, m_pool.m_opts.dust_relay_feerate, m_pool)}) {
const Txid& child_txid = ephemeral_violation.value();
TxValidationState child_state;
child_state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "missing-ephemeral-spends",
strprintf("tx %s did not spend parent's ephemeral dust", child_txid.ToString()));
package_state.Invalid(PackageValidationResult::PCKG_TX, "unspent-dust");
results.emplace(child_txid, MempoolAcceptResult::Failure(child_state));
return PackageMempoolAcceptResult(package_state, std::move(results));
}
}
for (Workspace& ws : workspaces) {
ws.m_package_feerate = package_feerate;
if (!PolicyScriptChecks(args, ws)) {