Skip to content

Commit d690f89

Browse files
committed
Merge bitcoin#28785: validation: return more helpful results for reconsiderable 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 bitcoin#26711 (suggested in bitcoin#26711 (comment)). This is part of bitcoin#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 bitcoin@1147e00 achow101: ACK 1147e00 murchandamus: reACK 1147e00 ismaelsadeeq: ACK 1147e00 Tree-SHA512: ac1cd73c2b487a1b99d329875d39d8107c91345a5b0b241d54a6a4de67faf11be69a2721cc732c503024a9cca381dac33d61e187957279e3c82653bea118ba91
2 parents 059f131 + 1147e00 commit d690f89

File tree

7 files changed

+281
-242
lines changed

7 files changed

+281
-242
lines changed

src/consensus/validation.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ enum class TxValidationResult {
5454
TX_CONFLICT,
5555
TX_MEMPOOL_POLICY, //!< violated mempool's fee/size/descendant/RBF/etc limits
5656
TX_NO_MEMPOOL, //!< this node does not have a mempool so can't validate the transaction
57+
TX_RECONSIDERABLE, //!< fails some policy, but might be acceptable if submitted in a (different) package
58+
TX_UNKNOWN, //!< transaction was not validated because package failed
5759
};
5860

5961
/** A "reason" why a block was invalid, suitable for determining whether the

src/net_processing.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,8 @@ bool PeerManagerImpl::MaybePunishNodeForTx(NodeId nodeid, const TxValidationStat
18201820
case TxValidationResult::TX_CONFLICT:
18211821
case TxValidationResult::TX_MEMPOOL_POLICY:
18221822
case TxValidationResult::TX_NO_MEMPOOL:
1823+
case TxValidationResult::TX_RECONSIDERABLE:
1824+
case TxValidationResult::TX_UNKNOWN:
18231825
break;
18241826
}
18251827
return false;

src/test/fuzz/tx_pool.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,15 @@ void CheckATMPInvariants(const MempoolAcceptResult& res, bool txid_in_mempool, b
154154
// It may be already in the mempool since in ATMP cases we don't set MEMPOOL_ENTRY or DIFFERENT_WITNESS
155155
Assert(!res.m_state.IsValid());
156156
Assert(res.m_state.IsInvalid());
157+
158+
const bool is_reconsiderable{res.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE};
157159
Assert(!res.m_replaced_transactions);
158160
Assert(!res.m_vsize);
159161
Assert(!res.m_base_fees);
160-
// Unable or unwilling to calculate fees
161-
Assert(!res.m_effective_feerate);
162-
Assert(!res.m_wtxids_fee_calculations);
162+
// Fee information is provided if the failure is TX_RECONSIDERABLE.
163+
// In other cases, validation may be unable or unwilling to calculate the fees.
164+
Assert(res.m_effective_feerate.has_value() == is_reconsiderable);
165+
Assert(res.m_wtxids_fee_calculations.has_value() == is_reconsiderable);
163166
Assert(!res.m_other_wtxid);
164167
break;
165168
}

src/test/txpackage_tests.cpp

Lines changed: 190 additions & 214 deletions
Large diffs are not rendered by default.

src/test/util/txmempool.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,14 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
8989
}
9090

9191
// m_effective_feerate and m_wtxids_fee_calculations should exist iff the result was valid
92-
if (atmp_result.m_effective_feerate.has_value() != valid) {
92+
// or if the failure was TX_RECONSIDERABLE
93+
const bool valid_or_reconsiderable{atmp_result.m_result_type == MempoolAcceptResult::ResultType::VALID ||
94+
atmp_result.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE};
95+
if (atmp_result.m_effective_feerate.has_value() != valid_or_reconsiderable) {
9396
return strprintf("tx %s result should %shave m_effective_feerate",
9497
wtxid.ToString(), valid ? "" : "not ");
9598
}
96-
if (atmp_result.m_wtxids_fee_calculations.has_value() != valid) {
99+
if (atmp_result.m_wtxids_fee_calculations.has_value() != valid_or_reconsiderable) {
97100
return strprintf("tx %s result should %shave m_effective_feerate",
98101
wtxid.ToString(), valid ? "" : "not ");
99102
}

src/validation.cpp

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -670,11 +670,11 @@ class MemPoolAccept
670670
AssertLockHeld(m_pool.cs);
671671
CAmount mempoolRejectFee = m_pool.GetMinFee().GetFee(package_size);
672672
if (mempoolRejectFee > 0 && package_fee < mempoolRejectFee) {
673-
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "mempool min fee not met", strprintf("%d < %d", package_fee, mempoolRejectFee));
673+
return state.Invalid(TxValidationResult::TX_RECONSIDERABLE, "mempool min fee not met", strprintf("%d < %d", package_fee, mempoolRejectFee));
674674
}
675675

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

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

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

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

1230-
if (!PreChecks(args, ws)) return MempoolAcceptResult::Failure(ws.m_state);
1240+
if (!PreChecks(args, ws)) {
1241+
if (ws.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE) {
1242+
// Failed for fee reasons. Provide the effective feerate and which tx was included.
1243+
return MempoolAcceptResult::FeeFailure(ws.m_state, CFeeRate(ws.m_modified_fees, ws.m_vsize), single_wtxid);
1244+
}
1245+
return MempoolAcceptResult::Failure(ws.m_state);
1246+
}
12311247

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

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

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

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

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

@@ -1299,11 +1319,16 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
12991319
const auto m_total_modified_fees = std::accumulate(workspaces.cbegin(), workspaces.cend(), CAmount{0},
13001320
[](CAmount sum, auto& ws) { return sum + ws.m_modified_fees; });
13011321
const CFeeRate package_feerate(m_total_modified_fees, m_total_vsize);
1322+
std::vector<Wtxid> all_package_wtxids;
1323+
all_package_wtxids.reserve(workspaces.size());
1324+
std::transform(workspaces.cbegin(), workspaces.cend(), std::back_inserter(all_package_wtxids),
1325+
[](const auto& ws) { return ws.m_ptx->GetWitnessHash(); });
13021326
TxValidationState placeholder_state;
13031327
if (args.m_package_feerates &&
13041328
!CheckFeeRate(m_total_vsize, m_total_modified_fees, placeholder_state)) {
1305-
package_state.Invalid(PackageValidationResult::PCKG_POLICY, "package-fee-too-low");
1306-
return PackageMempoolAcceptResult(package_state, {});
1329+
package_state.Invalid(PackageValidationResult::PCKG_TX, "transaction failed");
1330+
return PackageMempoolAcceptResult(package_state, {{workspaces.back().m_ptx->GetWitnessHash(),
1331+
MempoolAcceptResult::FeeFailure(placeholder_state, CFeeRate(m_total_modified_fees, m_total_vsize), all_package_wtxids)}});
13071332
}
13081333

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

1317-
std::vector<uint256> all_package_wtxids;
1318-
all_package_wtxids.reserve(workspaces.size());
1319-
std::transform(workspaces.cbegin(), workspaces.cend(), std::back_inserter(all_package_wtxids),
1320-
[](const auto& ws) { return ws.m_ptx->GetWitnessHash(); });
13211342
for (Workspace& ws : workspaces) {
13221343
ws.m_package_feerate = package_feerate;
13231344
if (!PolicyScriptChecks(args, ws)) {
@@ -1330,7 +1351,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptMultipleTransactions(const std::
13301351
const auto effective_feerate = args.m_package_feerates ? ws.m_package_feerate :
13311352
CFeeRate{ws.m_modified_fees, static_cast<uint32_t>(ws.m_vsize)};
13321353
const auto effective_feerate_wtxids = args.m_package_feerates ? all_package_wtxids :
1333-
std::vector<uint256>{ws.m_ptx->GetWitnessHash()};
1354+
std::vector<Wtxid>{ws.m_ptx->GetWitnessHash()};
13341355
results.emplace(ws.m_ptx->GetWitnessHash(),
13351356
MempoolAcceptResult::Success(std::move(ws.m_replaced_transactions),
13361357
ws.m_vsize, ws.m_base_fees, effective_feerate,
@@ -1510,7 +1531,7 @@ PackageMempoolAcceptResult MemPoolAccept::AcceptPackage(const Package& package,
15101531
// in package validation, because its fees should only be "used" once.
15111532
assert(m_pool.exists(GenTxid::Wtxid(wtxid)));
15121533
results_final.emplace(wtxid, single_res);
1513-
} else if (single_res.m_state.GetResult() != TxValidationResult::TX_MEMPOOL_POLICY &&
1534+
} else if (single_res.m_state.GetResult() != TxValidationResult::TX_RECONSIDERABLE &&
15141535
single_res.m_state.GetResult() != TxValidationResult::TX_MISSING_INPUTS) {
15151536
// Package validation policy only differs from individual policy in its evaluation
15161537
// of feerate. For example, if a transaction fails here due to violation of a

src/validation.h

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,27 @@ double GuessVerificationProgress(const ChainTxData& data, const CBlockIndex* pin
114114
void PruneBlockFilesManual(Chainstate& active_chainstate, int nManualPruneHeight);
115115

116116
/**
117-
* Validation result for a single transaction mempool acceptance.
117+
* Validation result for a transaction evaluated by MemPoolAccept (single or package).
118+
* Here are the expected fields and properties of a result depending on its ResultType, applicable to
119+
* results returned from package evaluation:
120+
*+---------------------------+----------------+-------------------+------------------+----------------+-------------------+
121+
*| Field or property | VALID | INVALID | MEMPOOL_ENTRY | DIFFERENT_WITNESS |
122+
*| | |--------------------------------------| | |
123+
*| | | TX_RECONSIDERABLE | Other | | |
124+
*+---------------------------+----------------+-------------------+------------------+----------------+-------------------+
125+
*| txid in mempool? | yes | no | no* | yes | yes |
126+
*| wtxid in mempool? | yes | no | no* | yes | no |
127+
*| m_state | yes, IsValid() | yes, IsInvalid() | yes, IsInvalid() | yes, IsValid() | yes, IsValid() |
128+
*| m_replaced_transactions | yes | no | no | no | no |
129+
*| m_vsize | yes | no | no | yes | no |
130+
*| m_base_fees | yes | no | no | yes | no |
131+
*| m_effective_feerate | yes | yes | no | no | no |
132+
*| m_wtxids_fee_calculations | yes | yes | no | no | no |
133+
*| m_other_wtxid | no | no | no | no | yes |
134+
*+---------------------------+----------------+-------------------+------------------+----------------+-------------------+
135+
* (*) Individual transaction acceptance doesn't return MEMPOOL_ENTRY and DIFFERENT_WITNESS. It returns
136+
* INVALID, with the errors txn-already-in-mempool and txn-same-nonwitness-data-in-mempool
137+
* respectively. In those cases, the txid or wtxid may be in the mempool for a TX_CONFLICT.
118138
*/
119139
struct MempoolAcceptResult {
120140
/** Used to indicate the results of mempool validation. */
@@ -130,7 +150,6 @@ struct MempoolAcceptResult {
130150
/** Contains information about why the transaction failed. */
131151
const TxValidationState m_state;
132152

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

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

158175
static MempoolAcceptResult Failure(TxValidationState state) {
159176
return MempoolAcceptResult(state);
160177
}
161178

179+
static MempoolAcceptResult FeeFailure(TxValidationState state,
180+
CFeeRate effective_feerate,
181+
const std::vector<Wtxid>& wtxids_fee_calculations) {
182+
return MempoolAcceptResult(state, effective_feerate, wtxids_fee_calculations);
183+
}
184+
162185
static MempoolAcceptResult Success(std::list<CTransactionRef>&& replaced_txns,
163186
int64_t vsize,
164187
CAmount fees,
165188
CFeeRate effective_feerate,
166-
const std::vector<uint256>& wtxids_fee_calculations) {
189+
const std::vector<Wtxid>& wtxids_fee_calculations) {
167190
return MempoolAcceptResult(std::move(replaced_txns), vsize, fees,
168191
effective_feerate, wtxids_fee_calculations);
169192
}
@@ -189,14 +212,23 @@ struct MempoolAcceptResult {
189212
int64_t vsize,
190213
CAmount fees,
191214
CFeeRate effective_feerate,
192-
const std::vector<uint256>& wtxids_fee_calculations)
215+
const std::vector<Wtxid>& wtxids_fee_calculations)
193216
: m_result_type(ResultType::VALID),
194217
m_replaced_transactions(std::move(replaced_txns)),
195218
m_vsize{vsize},
196219
m_base_fees(fees),
197220
m_effective_feerate(effective_feerate),
198221
m_wtxids_fee_calculations(wtxids_fee_calculations) {}
199222

223+
/** Constructor for fee-related failure case */
224+
explicit MempoolAcceptResult(TxValidationState state,
225+
CFeeRate effective_feerate,
226+
const std::vector<Wtxid>& wtxids_fee_calculations)
227+
: m_result_type(ResultType::INVALID),
228+
m_state(state),
229+
m_effective_feerate(effective_feerate),
230+
m_wtxids_fee_calculations(wtxids_fee_calculations) {}
231+
200232
/** Constructor for already-in-mempool case. It wouldn't replace any transactions. */
201233
explicit MempoolAcceptResult(int64_t vsize, CAmount fees)
202234
: m_result_type(ResultType::MEMPOOL_ENTRY), m_vsize{vsize}, m_base_fees(fees) {}

0 commit comments

Comments
 (0)