From fdf8e496b6751842d51e5f85c6389bb9834ac2e9 Mon Sep 17 00:00:00 2001 From: Greg Sanders Date: Thu, 6 Jun 2024 13:11:16 -0400 Subject: [PATCH] policy: Allow dust in transactions, as long as they are spent in-mempool Also known as ephemeral anchors. We try to ensure that dust is spent in blocks by requiring: - ephemeral anchor tx is 0-fee - ephemeral anchor tx is v3 (thus, can only have a single child bringing fees) - ephemeral anchor tx only has one dust output We do not take extra precautions to avoid dust in the case of a blockchain reorg. --- src/policy/v3_policy.cpp | 91 +++++++++++++++++++++++++++++ src/policy/v3_policy.h | 20 +++++++ src/validation.cpp | 62 ++++++++++++++++++-- test/functional/data/invalid_txs.py | 2 +- 4 files changed, 169 insertions(+), 6 deletions(-) diff --git a/src/policy/v3_policy.cpp b/src/policy/v3_policy.cpp index bcf0b2b23626cf..a3f4def5ae45aa 100644 --- a/src/policy/v3_policy.cpp +++ b/src/policy/v3_policy.cpp @@ -245,3 +245,94 @@ std::optional> SingleV3Checks(const CTra } return std::nullopt; } + +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; + + // Makes spending checks inference simple via topology restrictions, + // can be relaxed if spending checks can be done easier in future. + if (tx.version != TRUC_VERSION) { + return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "dust"); + } + + // 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 CheckEphemeralSpends(const Package& package, CFeeRate dust_relay_rate) +{ + assert(std::all_of(package.cbegin(), package.cend(), [](const auto& tx){return tx != nullptr;})); + + // Package is topologically sorted, and PreChecks ensures that + // there is up to one dust output per tx. Simply check if + // any are left unspent in this package. + std::unordered_set unspent_dust; + for (const auto& tx : package) { + for (uint32_t i=0; ivout.size(); i++) { + if (IsDust(tx->vout[i], dust_relay_rate)) { + unspent_dust.insert(COutPoint(tx->GetHash(), i)); + } + } + for (const auto& tx_input : tx->vin) { + unspent_dust.erase(tx_input.prevout); + } + } + + if (!unspent_dust.empty()) { + // Return something useful + return unspent_dust.begin()->hash; + } + + return std::nullopt; +} + +std::optional CheckEphemeralSpends(const CTransactionRef& ptx, + const CTxMemPool::setEntries& ancestors, + CFeeRate dust_relay_feerate) +{ + /* Ephemeral anchors are disallowed already, no need to check */ + if (ptx->version != TRUC_VERSION) { + return std::nullopt; + } + + std::unordered_set unspent_dust; + + // In the case of TRUC transactions, only one ancestor will be allowed anyways, + // but if relaxed to non-TRUC, this would need to be re-worked to check + // parents only. + for (const auto& entry : ancestors) { + const auto& tx = entry->GetTx(); + for (uint32_t i=0; ivin) { + unspent_dust.erase(input.prevout); + } + + if (!unspent_dust.empty()) { + return strprintf("tx does not spend all parent ephemeral anchors"); + } + + return std::nullopt; +} diff --git a/src/policy/v3_policy.h b/src/policy/v3_policy.h index 90eaeda46f18df..6ae1f1c483e05e 100644 --- a/src/policy/v3_policy.h +++ b/src/policy/v3_policy.h @@ -90,4 +90,24 @@ std::optional PackageV3Checks(const CTransactionRef& ptx, int64_t v const Package& package, const CTxMemPool::setEntries& mempool_ancestors); +/** Does context-less checks about a single transaction. + * If it has relay dust, it returns false if any are true: + * - tx is not V3 + * - 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 package ends up spent by package. Assumes package is well-formed and sorted. */ +std::optional CheckEphemeralSpends(const Package& package, CFeeRate dust_relay_rate); + +/** Checks that individual transactions' ancestors have all their dust spent by this transaction. + * Excluding reorgs, the ancestor set will be the direct parent only due to v3 restrictions. + */ +std::optional CheckEphemeralSpends(const CTransactionRef& ptx, + const CTxMemPool::setEntries& ancestors, + CFeeRate dust_relay_feerate); + #endif // BITCOIN_POLICY_V3_POLICY_H diff --git a/src/validation.cpp b/src/validation.cpp index 6db723d22c115e..3f8e3f37ce1487 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -481,6 +481,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& coins_to_uncache, @@ -496,6 +501,7 @@ class MemPoolAccept /* m_package_feerates */ false, /* m_client_maxfeerate */ {}, // checked by caller /* m_allow_carveouts */ true, + /* m_allow_ephemeral_dust */ false, }; } @@ -513,6 +519,7 @@ class MemPoolAccept /* m_package_feerates */ false, /* m_client_maxfeerate */ {}, // checked by caller /* m_allow_carveouts */ false, + /* m_allow_ephemeral_dust */ false, }; } @@ -530,6 +537,7 @@ class MemPoolAccept /* m_package_feerates */ true, /* m_client_maxfeerate */ client_maxfeerate, /* m_allow_carveouts */ false, + /* m_allow_ephemeral_dust */ true, }; } @@ -546,6 +554,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, }; } @@ -562,7 +571,8 @@ class MemPoolAccept bool package_submission, bool package_feerates, std::optional 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}, @@ -573,7 +583,8 @@ 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. @@ -581,6 +592,7 @@ class MemPoolAccept 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); } @@ -783,9 +795,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. @@ -925,6 +940,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)); @@ -1053,6 +1085,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-anchor-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 @@ -1451,6 +1490,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 auto parent_wtxid = ephemeral_violation.value(); + TxValidationState child_state; + child_state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "missing-ephemeral-spends", + strprintf("V3 tx %s has unspent ephemeral anchor", parent_wtxid.ToString())); + package_state.Invalid(PackageValidationResult::PCKG_TX, "unspent-dust"); + results.emplace(parent_wtxid, 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. diff --git a/test/functional/data/invalid_txs.py b/test/functional/data/invalid_txs.py index 33054fd5173e15..3dd1d3613be098 100644 --- a/test/functional/data/invalid_txs.py +++ b/test/functional/data/invalid_txs.py @@ -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