Skip to content

Commit

Permalink
policy: Allow dust in transactions, spent in-mempool
Browse files Browse the repository at this point in the history
Also known as Ephemeral Dust.

We try to ensure that dust is spent in blocks by requiring:
  - ephemeral dust tx is 0-fee
child bringing fees)
  - ephemeral dust tx only has one dust output
  - the output is spent by a single child transaction

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.
  • Loading branch information
instagibbs committed Jul 23, 2024
1 parent 8754d05 commit 092c1f4
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 6 deletions.
4 changes: 4 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ BITCOIN_CORE_H = \
node/warnings.h \
noui.h \
outputtype.h \
policy/ephemeral_policy.h \
policy/feerate.h \
policy/fees.h \
policy/fees_args.h \
Expand Down Expand Up @@ -445,6 +446,7 @@ libbitcoin_node_a_SOURCES = \
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 Expand Up @@ -705,6 +707,7 @@ libbitcoin_common_a_SOURCES = \
netbase.cpp \
net_permissions.cpp \
outputtype.cpp \
policy/ephemeral_policy.cpp \
policy/feerate.cpp \
policy/policy.cpp \
protocol.cpp \
Expand Down Expand Up @@ -951,6 +954,7 @@ libbitcoinkernel_la_SOURCES = \
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
126 changes: 126 additions & 0 deletions src/policy/ephemeral_policy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// 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 CheckValidEphemeralTx(const CTransaction& tx, CFeeRate dust_relay_fee, CAmount txfee, TxValidationState& state)
{
bool has_dust = false;
for (const CTxOut& txout : tx.vout) {
if (IsDust(txout, dust_relay_fee)) {
// We only allow a single dusty output
if (has_dust) {
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "dust");
}
has_dust = true;
}
}

// No dust; it's complete standard already
if (!has_dust) return true;

// We never want to give incentives to mine this alone
if (txfee != 0) {
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "dust");
}

return true;
}

std::optional<Txid> CheckEphemeralSpends(const Package& package, CFeeRate dust_relay_rate)
{
// Package is topologically sorted, and PreChecks ensures that
// there is up to one dust output per tx.

assert(std::all_of(package.cbegin(), package.cend(), [](const auto& tx){return tx != nullptr;}));

// Running tally of unspent dust
std::unordered_set<COutPoint, SaltedOutpointHasher> unspent_dust;

// If a parent tx has dust, we have to check for the spend
// Single dust per tx possible
std::map<Txid, uint32_t> map_tx_dust;

for (const auto& tx : package) {
std::unordered_set<Txid, SaltedTxidHasher> child_unspent_dust;
for (const auto& tx_input : tx->vin) {
// Parent tx had dust, child MUST be sweeping it
// if it's spending any output from parent
if (map_tx_dust.contains(tx_input.prevout.hash)) {
child_unspent_dust.insert(tx_input.prevout.hash);
}
}

// Now that we've built a list of parent txids
// that have dust, make sure that all parent's
// dust are swept by this same tx
for (const auto& tx_input : tx->vin) {
const auto& prevout = tx_input.prevout;
// Parent tx had dust, child MUST be sweeping it
// if it's spending any output from parent
if (map_tx_dust.contains(prevout.hash) &&
map_tx_dust[prevout.hash] == prevout.n) {
child_unspent_dust.erase(prevout.hash);
}

// We want to detect dangling dust too
unspent_dust.erase(tx_input.prevout);
}

if (!child_unspent_dust.empty()) {
return tx->GetHash();
}

// Process new dust
for (uint32_t i=0; i<tx->vout.size(); i++) {
if (IsDust(tx->vout[i], dust_relay_rate)) {
// CheckValidEphemeralTx should disallow multiples
Assume(!map_tx_dust.contains(tx->GetHash()));
map_tx_dust[tx->GetHash()] = i;
unspent_dust.insert(COutPoint(tx->GetHash(), i));
}
}

}

if (!unspent_dust.empty()) {
return unspent_dust.begin()->hash;
}

return std::nullopt;
}

std::optional<std::string> CheckEphemeralSpends(const CTransactionRef& ptx,
const CTxMemPool::setEntries& ancestors,
CFeeRate dust_relay_feerate)
{
std::unordered_set<COutPoint, SaltedOutpointHasher> unspent_dust;

std::unordered_set<Txid, SaltedTxidHasher> parents;
for (const auto& tx_input : ptx->vin) {
parents.insert(tx_input.prevout.hash);
}

for (const auto& entry : ancestors) {
const auto& tx = entry->GetTx();
// Only deal with direct parents
if (parents.count(tx.GetHash()) == 0) continue;
for (uint32_t i=0; i<tx.vout.size(); i++) {
if (IsDust(tx.vout[i], dust_relay_feerate)) {
unspent_dust.insert(COutPoint(tx.GetHash(), i));
}
}
}

for (const auto& input : ptx->vin) {
unspent_dust.erase(input.prevout);
}

if (!unspent_dust.empty()) {
return strprintf("tx does not spend parent ephemeral dust");
}

return std::nullopt;
}
57 changes: 57 additions & 0 deletions src/policy/ephemeral_policy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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 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 is the only transaction possible for
* bringing fees, or itself being spent by another child, and so on.
*/

/** Does context-less checks about a single transaction.
* If it has relay dust, it returns false if any are true:
* - tx has non-0 fee
- tx has more than one dust output
* and sets relevant invalid state.
* Otherwise it returns true.
*/
bool CheckValidEphemeralTx(const CTransaction& tx, CFeeRate dust_relay_fee, CAmount txfee, TxValidationState& state);

/** Checks that all dust in a package ends up spent by an only-child. Assumes package is well-formed and sorted.
* The function returns std::nullopt if all dust is properly spent, or the txid of a violated ephemeral transaction.
*/
std::optional<Txid> CheckEphemeralSpends(const Package& package, CFeeRate dust_relay_rate);

/** Checks that individual transactions' parents have all their dust spent by this only-child transaction.
*/
std::optional<std::string> CheckEphemeralSpends(const CTransactionRef& ptx,
const CTxMemPool::setEntries& ancestors,
CFeeRate dust_relay_feerate);

#endif // BITCOIN_POLICY_EPHEMERAL_POLICY_H
63 changes: 58 additions & 5 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -484,6 +485,11 @@ class MemPoolAccept
/** Whether CPFP carveout and RBF carveout are granted. */
const bool m_allow_carveouts;

/** Whether we allow dust inside PreChecks, since spentness checks will be handled
* later in AcceptMultipleTransactions.
*/
const bool m_allow_ephemeral_dust;

/** Parameters for single transaction mempool validation. */
static ATMPArgs SingleAccept(const CChainParams& chainparams, int64_t accept_time,
bool bypass_limits, std::vector<COutPoint>& coins_to_uncache,
Expand All @@ -499,6 +505,7 @@ class MemPoolAccept
/* m_package_feerates */ false,
/* m_client_maxfeerate */ {}, // checked by caller
/* m_allow_carveouts */ true,
/* m_allow_ephemeral_dust */ false,
};
}

Expand All @@ -516,6 +523,7 @@ class MemPoolAccept
/* m_package_feerates */ false,
/* m_client_maxfeerate */ {}, // checked by caller
/* m_allow_carveouts */ false,
/* m_allow_ephemeral_dust */ false,
};
}

Expand All @@ -533,6 +541,7 @@ class MemPoolAccept
/* m_package_feerates */ true,
/* m_client_maxfeerate */ client_maxfeerate,
/* m_allow_carveouts */ false,
/* m_allow_ephemeral_dust */ true,
};
}

Expand All @@ -549,6 +558,7 @@ class MemPoolAccept
/* m_package_feerates */ false, // only 1 transaction
/* m_client_maxfeerate */ package_args.m_client_maxfeerate,
/* m_allow_carveouts */ false,
/* m_allow_ephemeral_dust */ false,
};
}

Expand All @@ -565,7 +575,8 @@ class MemPoolAccept
bool package_submission,
bool package_feerates,
std::optional<CFeeRate> client_maxfeerate,
bool allow_carveouts)
bool allow_carveouts,
bool allow_epehemeral_dust)
: m_chainparams{chainparams},
m_accept_time{accept_time},
m_bypass_limits{bypass_limits},
Expand All @@ -576,14 +587,16 @@ class MemPoolAccept
m_package_submission{package_submission},
m_package_feerates{package_feerates},
m_client_maxfeerate{client_maxfeerate},
m_allow_carveouts{allow_carveouts}
m_allow_carveouts{allow_carveouts},
m_allow_ephemeral_dust{allow_epehemeral_dust}
{
// If we are using package feerates, we must be doing package submission.
// It also means carveouts and sibling eviction are not permitted.
if (m_package_feerates) {
Assume(m_package_submission);
Assume(!m_allow_carveouts);
Assume(!m_allow_sibling_eviction);
Assume(m_allow_ephemeral_dust);
}
if (m_allow_sibling_eviction) Assume(m_allow_replacement);
}
Expand Down Expand Up @@ -792,9 +805,12 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
return state.Invalid(TxValidationResult::TX_CONSENSUS, "coinbase");

// Rather not work on nonstandard transactions (unless -testnet/-regtest)
std::string reason;
if (m_pool.m_opts.require_standard && !IsStandardTx(tx, m_pool.m_opts.max_datacarrier_bytes, m_pool.m_opts.permit_bare_multisig, m_pool.m_opts.dust_relay_feerate, reason)) {
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, reason);
std::string std_reason;
if (m_pool.m_opts.require_standard &&
!IsStandardTx(tx, m_pool.m_opts.max_datacarrier_bytes, m_pool.m_opts.permit_bare_multisig, /*dust_relay_fee=*/CFeeRate(0), std_reason)) {
// Dust checks completed later
Assume(std_reason != "dust");
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, std_reason);
}

// Transactions smaller than 65 non-witness bytes are not relayed to mitigate CVE-2017-12842.
Expand Down Expand Up @@ -934,6 +950,23 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
fSpendsCoinbase, nSigOpsCost, lock_points.value()));
ws.m_vsize = entry->GetTxSize();

// Finalize dust checks at individual tx level
if (m_pool.m_opts.require_standard) {

// Dust was detected, but tx not valid format for ephemeral dust
if (!CheckValidEphemeralTx(tx, m_pool.m_opts.dust_relay_feerate, ws.m_base_fees, state)) {
return false; // state filled in by CheckValidEphemeralTx
}

// If there is an otherwise valid ephemeral dust, return TX_RECONSIDERABLE to allow retries in a package
if (!args.m_allow_ephemeral_dust &&
!bypass_limits &&
!IsStandardTx(tx, m_pool.m_opts.max_datacarrier_bytes, m_pool.m_opts.permit_bare_multisig, m_pool.m_opts.dust_relay_feerate, std_reason)) {
Assume(std_reason == "dust");
return state.Invalid(TxValidationResult::TX_RECONSIDERABLE, std_reason);
}
}

if (nSigOpsCost > MAX_STANDARD_TX_SIGOPS_COST)
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "bad-txns-too-many-sigops",
strprintf("%d", nSigOpsCost));
Expand Down Expand Up @@ -1062,6 +1095,13 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
}
}

// Ensure any parents in-mempool that have dust have it spent by this transaction
if (!bypass_limits && m_pool.m_opts.require_standard) {
if (auto err_string{CheckEphemeralSpends(ws.m_ptx, ws.m_ancestors, m_pool.m_opts.dust_relay_feerate)}) {
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "ephemeral-dust-unspent", *err_string);
}
}

// A transaction that spends outputs that would be replaced by it is invalid. Now
// that we have the set of all ancestors we can detect this
// pathological case by making sure ws.m_conflicts and ws.m_ancestors don't
Expand Down Expand Up @@ -1560,6 +1600,19 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
}
}

// Run package-based dust spentness checks
if (m_pool.m_opts.require_standard) {
if (const auto ephemeral_violation{CheckEphemeralSpends(txns, m_pool.m_opts.dust_relay_feerate)}) {
const Txid parent_txid = ephemeral_violation.value();
TxValidationState child_state;
child_state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "missing-ephemeral-spends",
strprintf("tx %s has unspent ephemeral dust", parent_txid.ToString()));
package_state.Invalid(PackageValidationResult::PCKG_TX, "unspent-dust");
results.emplace(parent_txid, MempoolAcceptResult::Failure(child_state));
return PackageMempoolAcceptResult(package_state, std::move(results));
}
}

// Transactions must meet two minimum feerates: the mempool minimum fee and min relay fee.
// For transactions consisting of exactly one child and its parents, it suffices to use the
// package feerate (total modified fees / total virtual size) to check this requirement.
Expand Down
2 changes: 1 addition & 1 deletion test/functional/data/invalid_txs.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def get_tx(self):
vin = self.valid_txin
vin.scriptSig = CScript([opcode])
tx.vin.append(vin)
tx.vout.append(CTxOut(1, basic_p2sh))
tx.vout.append(CTxOut(1000, basic_p2sh))
tx.calc_sha256()
return tx

Expand Down

0 comments on commit 092c1f4

Please sign in to comment.