mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-09 19:37:27 -03:00
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:
parent
04b2714fbb
commit
e1d3e81ab4
8 changed files with 187 additions and 2 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
78
src/policy/ephemeral_policy.cpp
Normal file
78
src/policy/ephemeral_policy.cpp
Normal 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;
|
||||
}
|
55
src/policy/ephemeral_policy.h
Normal file
55
src/policy/ephemeral_policy.h
Normal 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
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)) {
|
||||
|
|
Loading…
Reference in a new issue