Skip to content

Commit

Permalink
Merge #28785: validation: return more helpful results for reconsidera…
Browse files Browse the repository at this point in the history
…ble fee failures and skipped transactions

1147e00 [validation] change package-fee-too-low, return wtxid(s) and effective feerate (glozow)
10dd9f2 [test] use CheckPackageMempoolAcceptResult in previous tests (glozow)
3979f1a [validation] add TxValidationResult::TX_RECONSIDERABLE, TX_UNKNOWN (glozow)
5c786a0 [refactor] use Wtxid for m_wtxids_fee_calculations (glozow)

Pull request description:

  Split off from #26711 (suggested in #26711 (comment)). This is part of #27463.

  - Add 2 new TxValidationResults
    - `TX_RECONSIDERABLE` helps us encode transactions who have failed fee checks that can be bypassed using package validation. This is distinguished from `TX_MEMPOOL_POLICY` so that we re-validate a transaction if and only if it is eligible for package CPFP. In the future, we will have a separate cache for reconsiderable rejects so these transactions don't go in `m_recent_rejects`.
    - `TX_UNKNOWN` helps us communicate that we aborted package validation and didn't finish looking at this transaction: it's not valid but it's also not invalid (i.e. don't cache it as a rejected tx)
  - Return effective feerate and the wtxids of transactions used to calculate that effective feerate when the error is `TX_SINGLE_FAILURE`. Previously, we would only provide this information if the transaction passed. Now that we have package validation, it's much more helpful to the caller to know how the failing feerate was calculated. This can also be used to improve our submitpackage RPC result (which is currently a bit unhelpful when things fail).
  - Use the newly added `CheckPackageMempoolAcceptResult` for existing package validation tests. This increases test coverage and helps test the changes made in this PR.

ACKs for top commit:
  instagibbs:
    reACK 1147e00
  achow101:
    ACK 1147e00
  murchandamus:
    reACK 1147e00
  ismaelsadeeq:
    ACK 1147e00

Tree-SHA512: ac1cd73c2b487a1b99d329875d39d8107c91345a5b0b241d54a6a4de67faf11be69a2721cc732c503024a9cca381dac33d61e187957279e3c82653bea118ba91
  • Loading branch information
fanquake committed Nov 8, 2023
2 parents 059f131 + 1147e00 commit d690f89
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 242 deletions.
2 changes: 2 additions & 0 deletions src/consensus/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ enum class TxValidationResult {
TX_CONFLICT,
TX_MEMPOOL_POLICY, //!< violated mempool's fee/size/descendant/RBF/etc limits
TX_NO_MEMPOOL, //!< this node does not have a mempool so can't validate the transaction
TX_RECONSIDERABLE, //!< fails some policy, but might be acceptable if submitted in a (different) package
TX_UNKNOWN, //!< transaction was not validated because package failed
};

/** A "reason" why a block was invalid, suitable for determining whether the
Expand Down
2 changes: 2 additions & 0 deletions src/net_processing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1820,6 +1820,8 @@ bool PeerManagerImpl::MaybePunishNodeForTx(NodeId nodeid, const TxValidationStat
case TxValidationResult::TX_CONFLICT:
case TxValidationResult::TX_MEMPOOL_POLICY:
case TxValidationResult::TX_NO_MEMPOOL:
case TxValidationResult::TX_RECONSIDERABLE:
case TxValidationResult::TX_UNKNOWN:
break;
}
return false;
Expand Down
9 changes: 6 additions & 3 deletions src/test/fuzz/tx_pool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,15 @@ void CheckATMPInvariants(const MempoolAcceptResult& res, bool txid_in_mempool, b
// It may be already in the mempool since in ATMP cases we don't set MEMPOOL_ENTRY or DIFFERENT_WITNESS
Assert(!res.m_state.IsValid());
Assert(res.m_state.IsInvalid());

const bool is_reconsiderable{res.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE};
Assert(!res.m_replaced_transactions);
Assert(!res.m_vsize);
Assert(!res.m_base_fees);
// Unable or unwilling to calculate fees
Assert(!res.m_effective_feerate);
Assert(!res.m_wtxids_fee_calculations);
// Fee information is provided if the failure is TX_RECONSIDERABLE.
// In other cases, validation may be unable or unwilling to calculate the fees.
Assert(res.m_effective_feerate.has_value() == is_reconsiderable);
Assert(res.m_wtxids_fee_calculations.has_value() == is_reconsiderable);
Assert(!res.m_other_wtxid);
break;
}
Expand Down
404 changes: 190 additions & 214 deletions src/test/txpackage_tests.cpp

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions src/test/util/txmempool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,14 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
}

// m_effective_feerate and m_wtxids_fee_calculations should exist iff the result was valid
if (atmp_result.m_effective_feerate.has_value() != valid) {
// or if the failure was TX_RECONSIDERABLE
const bool valid_or_reconsiderable{atmp_result.m_result_type == MempoolAcceptResult::ResultType::VALID ||
atmp_result.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE};
if (atmp_result.m_effective_feerate.has_value() != valid_or_reconsiderable) {
return strprintf("tx %s result should %shave m_effective_feerate",
wtxid.ToString(), valid ? "" : "not ");
}
if (atmp_result.m_wtxids_fee_calculations.has_value() != valid) {
if (atmp_result.m_wtxids_fee_calculations.has_value() != valid_or_reconsiderable) {
return strprintf("tx %s result should %shave m_effective_feerate",
wtxid.ToString(), valid ? "" : "not ");
}
Expand Down
53 changes: 37 additions & 16 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -670,11 +670,11 @@ class MemPoolAccept
AssertLockHeld(m_pool.cs);
CAmount mempoolRejectFee = m_pool.GetMinFee().GetFee(package_size);
if (mempoolRejectFee > 0 && package_fee < mempoolRejectFee) {
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "mempool min fee not met", strprintf("%d < %d", package_fee, mempoolRejectFee));
return state.Invalid(TxValidationResult::TX_RECONSIDERABLE, "mempool min fee not met", strprintf("%d < %d", package_fee, mempoolRejectFee));
}

if (package_fee < m_pool.m_min_relay_feerate.GetFee(package_size)) {
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "min relay fee not met",
return state.Invalid(TxValidationResult::TX_RECONSIDERABLE, "min relay fee not met",
strprintf("%d < %d", package_fee, m_pool.m_min_relay_feerate.GetFee(package_size)));
}
return true;
Expand Down Expand Up @@ -867,6 +867,8 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
// method of ensuring the tx remains bumped. For example, the fee-bumping child could disappear
// due to a replacement.
if (!bypass_limits && ws.m_modified_fees < m_pool.m_min_relay_feerate.GetFee(ws.m_vsize)) {
// Even though this is a fee-related failure, this result is TX_MEMPOOL_POLICY, not
// TX_RECONSIDERABLE, because it cannot be bypassed using package validation.
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "min relay fee not met",
strprintf("%d < %d", ws.m_modified_fees, m_pool.m_min_relay_feerate.GetFee(ws.m_vsize)));
}
Expand Down Expand Up @@ -981,6 +983,9 @@ bool MemPoolAccept::ReplacementChecks(Workspace& ws)
// descendant transaction of a direct conflict to pay a higher feerate than the transaction that
// might replace them, under these rules.
if (const auto err_string{PaysMoreThanConflicts(ws.m_iters_conflicting, newFeeRate, hash)}) {
// Even though this is a fee-related failure, this result is TX_MEMPOOL_POLICY, not
// TX_RECONSIDERABLE, because it cannot be bypassed using package validation.
// This must be changed if package RBF is enabled.
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "insufficient fee", *err_string);
}

Expand All @@ -1002,6 +1007,9 @@ bool MemPoolAccept::ReplacementChecks(Workspace& ws)
}
if (const auto err_string{PaysForRBF(ws.m_conflicting_fees, ws.m_modified_fees, ws.m_vsize,
m_pool.m_incremental_relay_feerate, hash)}) {
// Even though this is a fee-related failure, this result is TX_MEMPOOL_POLICY, not
// TX_RECONSIDERABLE, because it cannot be bypassed using package validation.
// This must be changed if package RBF is enabled.
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "insufficient fee", *err_string);
}
return true;
Expand Down Expand Up @@ -1139,7 +1147,8 @@ bool MemPoolAccept::Finalize(const ATMPArgs& args, Workspace& ws)
if (!args.m_package_submission && !bypass_limits) {
LimitMempoolSize(m_pool, m_active_chainstate.CoinsTip());
if (!m_pool.exists(GenTxid::Txid(hash)))
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "mempool full");
// The tx no longer meets our (new) mempool minimum feerate but could be reconsidered in a package.
return state.Invalid(TxValidationResult::TX_RECONSIDERABLE, "mempool full");
}
return true;
}
Expand Down Expand Up @@ -1201,7 +1210,7 @@ bool MemPoolAccept::SubmitPackage(const ATMPArgs& args, std::vector<Workspace>&
}
}

std::vector<uint256> all_package_wtxids;
std::vector<Wtxid> all_package_wtxids;
all_package_wtxids.reserve(workspaces.size());
std::transform(workspaces.cbegin(), workspaces.cend(), std::back_inserter(all_package_wtxids),
[](const auto& ws) { return ws.m_ptx->GetWitnessHash(); });
Expand All @@ -1211,7 +1220,7 @@ bool MemPoolAccept::SubmitPackage(const ATMPArgs& args, std::vector<Workspace>&
const auto effective_feerate = args.m_package_feerates ? ws.m_package_feerate :
CFeeRate{ws.m_modified_fees, static_cast<uint32_t>(ws.m_vsize)};
const auto effective_feerate_wtxids = args.m_package_feerates ? all_package_wtxids :
std::vector<uint256>({ws.m_ptx->GetWitnessHash()});
std::vector<Wtxid>{ws.m_ptx->GetWitnessHash()};
results.emplace(ws.m_ptx->GetWitnessHash(),
MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize,
ws.m_base_fees, effective_feerate, effective_feerate_wtxids));
Expand All @@ -1226,8 +1235,15 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
LOCK(m_pool.cs); // mempool "read lock" (held through GetMainSignals().TransactionAddedToMempool())

Workspace ws(ptx);
const std::vector<Wtxid> single_wtxid{ws.m_ptx->GetWitnessHash()};

if (!PreChecks(args, ws)) return MempoolAcceptResult::Failure(ws.m_state);
if (!PreChecks(args, ws)) {
if (ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE) {
// Failed for fee reasons. Provide the effective feerate and which tx was included.
return MempoolAcceptResult::FeeFailure(ws.m_state, CFeeRate(ws.m_modified_fees, ws.m_vsize), single_wtxid);
}
return MempoolAcceptResult::Failure(ws.m_state);
}

if (m_rbf && !ReplacementChecks(ws)) return MempoolAcceptResult::Failure(ws.m_state);

Expand All @@ -1238,14 +1254,18 @@ MempoolAcceptResult MemPoolAccept::AcceptSingleTransaction(const CTransactionRef
if (!ConsensusScriptChecks(args, ws)) return MempoolAcceptResult::Failure(ws.m_state);

const CFeeRate effective_feerate{ws.m_modified_fees, static_cast<uint32_t>(ws.m_vsize)};
const std::vector<uint256> single_wtxid{ws.m_ptx->GetWitnessHash()};
// Tx was accepted, but not added
if (args.m_test_accept) {
return MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions), ws.m_vsize,
ws.m_base_fees, effective_feerate, single_wtxid);
}

if (!Finalize(args, ws)) return MempoolAcceptResult::Failure(ws.m_state);
if (!Finalize(args, ws)) {
// The only possible failure reason is fee-related (mempool full).
// Failed for fee reasons. Provide the effective feerate and which txns were included.
Assume(ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE);
return MempoolAcceptResult::FeeFailure(ws.m_state, CFeeRate(ws.m_modified_fees, ws.m_vsize), {ws.m_ptx->GetWitnessHash()});
}

GetMainSignals().TransactionAddedToMempool(ptx, m_pool.GetAndIncrementSequence());

Expand Down Expand Up @@ -1299,11 +1319,16 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
const auto m_total_modified_fees = std::accumulate(workspaces.cbegin(), workspaces.cend(), CAmount{0},
[](CAmount sum, auto& ws) { return sum + ws.m_modified_fees; });
const CFeeRate package_feerate(m_total_modified_fees, m_total_vsize);
std::vector<Wtxid> all_package_wtxids;
all_package_wtxids.reserve(workspaces.size());
std::transform(workspaces.cbegin(), workspaces.cend(), std::back_inserter(all_package_wtxids),
[](const auto& ws) { return ws.m_ptx->GetWitnessHash(); });
TxValidationState placeholder_state;
if (args.m_package_feerates &&
!CheckFeeRate(m_total_vsize, m_total_modified_fees, placeholder_state)) {
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-fee-too-low");
return PackageMempoolAcceptResult(package_state, {});
package_state.Invalid(PackageValidationResult::PCKG_TX, "transaction failed");
return PackageMempoolAcceptResult(package_state, {{workspaces.back().m_ptx->GetWitnessHash(),
MempoolAcceptResult::FeeFailure(placeholder_state, CFeeRate(m_total_modified_fees, m_total_vsize), all_package_wtxids)}});
}

// Apply package mempool ancestor/descendant limits. Skip if there is only one transaction,
Expand All @@ -1314,10 +1339,6 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
return PackageMempoolAcceptResult(package_state, std::move(results));
}

std::vector<uint256> all_package_wtxids;
all_package_wtxids.reserve(workspaces.size());
std::transform(workspaces.cbegin(), workspaces.cend(), std::back_inserter(all_package_wtxids),
[](const auto& ws) { return ws.m_ptx->GetWitnessHash(); });
for (Workspace& ws : workspaces) {
ws.m_package_feerate = package_feerate;
if (!PolicyScriptChecks(args, ws)) {
Expand All @@ -1330,7 +1351,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
const auto effective_feerate = args.m_package_feerates ? ws.m_package_feerate :
CFeeRate{ws.m_modified_fees, static_cast<uint32_t>(ws.m_vsize)};
const auto effective_feerate_wtxids = args.m_package_feerates ? all_package_wtxids :
std::vector<uint256>{ws.m_ptx->GetWitnessHash()};
std::vector<Wtxid>{ws.m_ptx->GetWitnessHash()};
results.emplace(ws.m_ptx->GetWitnessHash(),
MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions),
ws.m_vsize, ws.m_base_fees, effective_feerate,
Expand Down Expand Up @@ -1510,7 +1531,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
// in package validation, because its fees should only be "used" once.
assert(m_pool.exists(GenTxid::Wtxid(wtxid)));
results_final.emplace(wtxid, single_res);
} else if (single_res.m_state.GetResult() != TxValidationResult::TX_MEMPOOL_POLICY &&
} else if (single_res.m_state.GetResult() != TxValidationResult::TX_RECONSIDERABLE &&
single_res.m_state.GetResult() != TxValidationResult::TX_MISSING_INPUTS) {
// Package validation policy only differs from individual policy in its evaluation
// of feerate. For example, if a transaction fails here due to violation of a
Expand Down
46 changes: 39 additions & 7 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,27 @@ double GuessVerificationProgress(const ChainTxData& data, const CBlockIndex* pin
void PruneBlockFilesManual(Chainstate& active_chainstate, int nManualPruneHeight);

/**
* Validation result for a single transaction mempool acceptance.
* Validation result for a transaction evaluated by MemPoolAccept (single or package).
* Here are the expected fields and properties of a result depending on its ResultType, applicable to
* results returned from package evaluation:
*+---------------------------+----------------+-------------------+------------------+----------------+-------------------+
*| Field or property | VALID | INVALID | MEMPOOL_ENTRY | DIFFERENT_WITNESS |
*| | |--------------------------------------| | |
*| | | TX_RECONSIDERABLE | Other | | |
*+---------------------------+----------------+-------------------+------------------+----------------+-------------------+
*| txid in mempool? | yes | no | no* | yes | yes |
*| wtxid in mempool? | yes | no | no* | yes | no |
*| m_state | yes, IsValid() | yes, IsInvalid() | yes, IsInvalid() | yes, IsValid() | yes, IsValid() |
*| m_replaced_transactions | yes | no | no | no | no |
*| m_vsize | yes | no | no | yes | no |
*| m_base_fees | yes | no | no | yes | no |
*| m_effective_feerate | yes | yes | no | no | no |
*| m_wtxids_fee_calculations | yes | yes | no | no | no |
*| m_other_wtxid | no | no | no | no | yes |
*+---------------------------+----------------+-------------------+------------------+----------------+-------------------+
* (*) Individual transaction acceptance doesn't return MEMPOOL_ENTRY and DIFFERENT_WITNESS. It returns
* INVALID, with the errors txn-already-in-mempool and txn-same-nonwitness-data-in-mempool
* respectively. In those cases, the txid or wtxid may be in the mempool for a TX_CONFLICT.
*/
struct MempoolAcceptResult {
/** Used to indicate the results of mempool validation. */
Expand All @@ -130,7 +150,6 @@ struct MempoolAcceptResult {
/** Contains information about why the transaction failed. */
const TxValidationState m_state;

// The following fields are only present when m_result_type = ResultType::VALID or MEMPOOL_ENTRY
/** Mempool transactions replaced by the tx. */
const std::optional<std::list<CTransactionRef>> m_replaced_transactions;
/** Virtual size as used by the mempool, calculated using serialized size and sigops. */
Expand All @@ -141,29 +160,33 @@ struct MempoolAcceptResult {
* using prioritisetransaction (i.e. modified fees). If this transaction was submitted as a
* package, this is the package feerate, which may also include its descendants and/or
* ancestors (see m_wtxids_fee_calculations below).
* Only present when m_result_type = ResultType::VALID.
*/
const std::optional<CFeeRate> m_effective_feerate;
/** Contains the wtxids of the transactions used for fee-related checks. Includes this
* transaction's wtxid and may include others if this transaction was validated as part of a
* package. This is not necessarily equivalent to the list of transactions passed to
* ProcessNewPackage().
* Only present when m_result_type = ResultType::VALID. */
const std::optional<std::vector<uint256>> m_wtxids_fee_calculations;
const std::optional<std::vector<Wtxid>> m_wtxids_fee_calculations;

// The following field is only present when m_result_type = ResultType::DIFFERENT_WITNESS
/** The wtxid of the transaction in the mempool which has the same txid but different witness. */
const std::optional<uint256> m_other_wtxid;

static MempoolAcceptResult Failure(TxValidationState state) {
return MempoolAcceptResult(state);
}

static MempoolAcceptResult FeeFailure(TxValidationState state,
CFeeRate effective_feerate,
const std::vector<Wtxid>& wtxids_fee_calculations) {
return MempoolAcceptResult(state, effective_feerate, wtxids_fee_calculations);
}

static MempoolAcceptResult Success(std::list<CTransactionRef>&& replaced_txns,
int64_t vsize,
CAmount fees,
CFeeRate effective_feerate,
const std::vector<uint256>& wtxids_fee_calculations) {
const std::vector<Wtxid>& wtxids_fee_calculations) {
return MempoolAcceptResult(std::move(replaced_txns), vsize, fees,
effective_feerate, wtxids_fee_calculations);
}
Expand All @@ -189,14 +212,23 @@ struct MempoolAcceptResult {
int64_t vsize,
CAmount fees,
CFeeRate effective_feerate,
const std::vector<uint256>& wtxids_fee_calculations)
const std::vector<Wtxid>& wtxids_fee_calculations)
: m_result_type(ResultType::VALID),
m_replaced_transactions(std::move(replaced_txns)),
m_vsize{vsize},
m_base_fees(fees),
m_effective_feerate(effective_feerate),
m_wtxids_fee_calculations(wtxids_fee_calculations) {}

/** Constructor for fee-related failure case */
explicit MempoolAcceptResult(TxValidationState state,
CFeeRate effective_feerate,
const std::vector<Wtxid>& wtxids_fee_calculations)
: m_result_type(ResultType::INVALID),
m_state(state),
m_effective_feerate(effective_feerate),
m_wtxids_fee_calculations(wtxids_fee_calculations) {}

/** Constructor for already-in-mempool case. It wouldn't replace any transactions. */
explicit MempoolAcceptResult(int64_t vsize, CAmount fees)
: m_result_type(ResultType::MEMPOOL_ENTRY), m_vsize{vsize}, m_base_fees(fees) {}
Expand Down

0 comments on commit d690f89

Please sign in to comment.