Skip to content

Commit

Permalink
policy: Allow dust in transactions, as long as they are spent in-mempool
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
instagibbs committed Jun 17, 2024
1 parent 2c79abc commit ca9466f
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 5 deletions.
91 changes: 91 additions & 0 deletions src/policy/v3_policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,94 @@ std::optional<std::pair<std::string, CTransactionRef>> 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<uint256> 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<COutPoint, SaltedOutpointHasher> unspent_dust;
for (const auto& tx : package) {
for (uint32_t i=0; i<tx->vout.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<std::string> 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<COutPoint, SaltedOutpointHasher> 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; 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 all parent ephemeral anchors");
}

return std::nullopt;
}
20 changes: 20 additions & 0 deletions src/policy/v3_policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,24 @@ std::optional<std::string> 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<uint256> 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<std::string> CheckEphemeralSpends(const CTransactionRef& ptx,
const CTxMemPool::setEntries& ancestors,
CFeeRate dust_relay_feerate);

#endif // BITCOIN_POLICY_V3_POLICY_H
60 changes: 56 additions & 4 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<COutPoint>& coins_to_uncache,
Expand All @@ -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,
};
}

Expand All @@ -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,
};
}

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

Expand All @@ -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,
};
}

Expand All @@ -562,7 +571,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 @@ -573,14 +583,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 @@ -784,8 +796,16 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)

// 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);
// We allow dust if it's spent in package, with further tightening via CheckValidEphemeralTx
bool dust_failure = false;
const auto dust_relay_feerate = bypass_limits || args.m_allow_ephemeral_dust ? CFeeRate(0) : m_pool.m_opts.dust_relay_feerate;
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_feerate, reason)) {
if (reason == "dust") {
Assume(!args.m_allow_ephemeral_dust);
dust_failure = true;
} else {
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, reason);
}
}

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

// Now that we know size and fees, we can report reconsiderable dust failure
if (dust_failure) {
return state.Invalid(TxValidationResult::TX_RECONSIDERABLE, "dust");
}

// We might have dust, but we're in package context and will check spentness later via CheckEphemeralSpends
if (m_pool.m_opts.require_standard &&
!bypass_limits &&
!CheckValidEphemeralTx(tx, m_pool.m_opts.dust_relay_feerate, ws.m_base_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));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
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 ca9466f

Please sign in to comment.