Skip to content

Commit 7257e50

Browse files
committed
Merge bitcoin/bitcoin#20833: rpc/validation: enable packages through testmempoolaccept
13650fe [policy] detect unsorted packages (glozow) 9ef643e [doc] add release note for package testmempoolaccept (glozow) c4259f4 [test] functional test for packages in RPCs (glozow) 9ede34a [rpc] allow multiple txns in testmempoolaccept (glozow) ae8e6df [policy] limit package sizes (glozow) c9e1a26 [fuzz] add ProcessNewPackage call in tx_pool fuzzer (glozow) 363e3d9 [test] unit tests for ProcessNewPackage (glozow) cd9a11a [test] make submit optional in CreateValidMempoolTransaction (glozow) 2ef1879 [validation] package validation for test accepts (glozow) 578148d [validation] explicit Success/Failure ctors for MempoolAcceptResult (glozow) b88d77a [policy] Define packages (glozow) 249f43f [refactor] add option to disable RBF (glozow) 897e348 [coins/mempool] extend CCoinsViewMemPool to track temporary coins (glozow) 42cf8b2 [validation] make CheckSequenceLocks context-free (glozow) Pull request description: This PR enables validation dry-runs of packages through the `testmempoolaccept` RPC. The expectation is that the results returned from `testmempoolaccept` are what you'd get from test-then-submitting each transaction individually, in that order (this means the package is expected to be sorted in topological order, for now at least). The validation is also atomic: in the case of failure, it immediately halts and may return "unfinished" `MempoolAcceptResult`s for transactions that weren't fully validated. The API for 1 transaction stays the same. **Motivation:** - This allows you to test validity for transaction chains (e.g. with multiple spending paths and where you don't want to broadcast yet); closes #18480. - It's also a first step towards package validation in a minimally invasive way. - The RPC commit happens to close #21074 by clarifying the "allowed" key. There are a few added restrictions on the packages, mostly to simplify the logic for areas that aren't critical to main package use cases: - No package can have conflicts, i.e. none of them can spend the same inputs, even if it would be a valid BIP125 replacement. - The package cannot conflict with the mempool, i.e. RBF is disabled. - The total count of the package cannot exceed 25 (the default descendant count limit), and total size cannot exceed 101KvB (the default descendant size limit). If you're looking for review comments and github isn't loading them, I have a gist compiling some topics of discussion [here](https://gist.github.com/glozow/c3acaf161c95bba491fce31585b2aaf7) ACKs for top commit: laanwj: Code review re-ACK 13650fe jnewbery: Code review ACK 13650fe ariard: ACK 13650fe Tree-SHA512: 8c5cbfa91a6c714e1c8710bb281d5ff1c5af36741872a7c5df6b24874d6272b4a09f816cb8a4c7de33ef8e1c2a2c252c0df5105b7802f70bc6ff821ed7cc1a2f
2 parents 2e8f392 + 13650fe commit 7257e50

16 files changed

+841
-87
lines changed

doc/release-notes-20833.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Updated RPCs
2+
------------
3+
4+
- The `testmempoolaccept` RPC now accepts multiple transactions (still experimental at the moment,
5+
API may be unstable). This is intended for testing transaction packages with dependency
6+
relationships; it is not recommended for batch-validating independent transactions. In addition to
7+
mempool policy, package policies apply: the list cannot contain more than 25 transactions or have a
8+
total size exceeding 101K virtual bytes, and cannot conflict with (spend the same inputs as) each other or
9+
the mempool, even if it would be a valid BIP125 replace-by-fee. There are some known limitations to
10+
the accuracy of the test accept: it's possible for `testmempoolaccept` to return "allowed"=True for a
11+
group of transactions, but "too-long-mempool-chain" if they are actually submitted. (#20833)
12+

src/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ BITCOIN_CORE_H = \
193193
outputtype.h \
194194
policy/feerate.h \
195195
policy/fees.h \
196+
policy/packages.h \
196197
policy/policy.h \
197198
policy/rbf.h \
198199
policy/settings.h \

src/policy/packages.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2021 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_POLICY_PACKAGES_H
6+
#define BITCOIN_POLICY_PACKAGES_H
7+
8+
#include <consensus/validation.h>
9+
#include <primitives/transaction.h>
10+
11+
#include <vector>
12+
13+
/** Default maximum number of transactions in a package. */
14+
static constexpr uint32_t MAX_PACKAGE_COUNT{25};
15+
/** Default maximum total virtual size of transactions in a package in KvB. */
16+
static constexpr uint32_t MAX_PACKAGE_SIZE{101};
17+
18+
/** A "reason" why a package was invalid. It may be that one or more of the included
19+
* transactions is invalid or the package itself violates our rules.
20+
* We don't distinguish between consensus and policy violations right now.
21+
*/
22+
enum class PackageValidationResult {
23+
PCKG_RESULT_UNSET = 0, //!< Initial value. The package has not yet been rejected.
24+
PCKG_POLICY, //!< The package itself is invalid (e.g. too many transactions).
25+
PCKG_TX, //!< At least one tx is invalid.
26+
};
27+
28+
/** A package is an ordered list of transactions. The transactions cannot conflict with (spend the
29+
* same inputs as) one another. */
30+
using Package = std::vector<CTransactionRef>;
31+
32+
class PackageValidationState : public ValidationState<PackageValidationResult> {};
33+
34+
#endif // BITCOIN_POLICY_PACKAGES_H

src/rpc/rawtransaction.cpp

Lines changed: 81 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <node/context.h>
1616
#include <node/psbt.h>
1717
#include <node/transaction.h>
18+
#include <policy/packages.h>
1819
#include <policy/policy.h>
1920
#include <policy/rbf.h>
2021
#include <primitives/transaction.h>
@@ -885,8 +886,11 @@ static RPCHelpMan sendrawtransaction()
885886
static RPCHelpMan testmempoolaccept()
886887
{
887888
return RPCHelpMan{"testmempoolaccept",
888-
"\nReturns result of mempool acceptance tests indicating if raw transaction (serialized, hex-encoded) would be accepted by mempool.\n"
889-
"\nThis checks if the transaction violates the consensus or policy rules.\n"
889+
"\nReturns result of mempool acceptance tests indicating if raw transaction(s) (serialized, hex-encoded) would be accepted by mempool.\n"
890+
"\nIf multiple transactions are passed in, parents must come before children and package policies apply: the transactions cannot conflict with any mempool transactions or each other.\n"
891+
"\nIf one transaction fails, other transactions may not be fully validated (the 'allowed' key will be blank).\n"
892+
"\nThe maximum number of transactions allowed is 25 (MAX_PACKAGE_COUNT)\n"
893+
"\nThis checks if transactions violate the consensus or policy rules.\n"
890894
"\nSee sendrawtransaction call.\n",
891895
{
892896
{"rawtxs", RPCArg::Type::ARR, RPCArg::Optional::NO, "An array of hex strings of raw transactions.\n"
@@ -895,17 +899,21 @@ static RPCHelpMan testmempoolaccept()
895899
{"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""},
896900
},
897901
},
898-
{"maxfeerate", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK())}, "Reject transactions whose fee rate is higher than the specified value, expressed in " + CURRENCY_UNIT + "/kvB\n"},
902+
{"maxfeerate", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK())},
903+
"Reject transactions whose fee rate is higher than the specified value, expressed in " + CURRENCY_UNIT + "/kvB\n"},
899904
},
900905
RPCResult{
901906
RPCResult::Type::ARR, "", "The result of the mempool acceptance test for each raw transaction in the input array.\n"
902-
"Length is exactly one for now.",
907+
"Returns results for each transaction in the same order they were passed in.\n"
908+
"It is possible for transactions to not be fully validated ('allowed' unset) if an earlier transaction failed.\n",
903909
{
904910
{RPCResult::Type::OBJ, "", "",
905911
{
906912
{RPCResult::Type::STR_HEX, "txid", "The transaction hash in hex"},
907913
{RPCResult::Type::STR_HEX, "wtxid", "The transaction witness hash in hex"},
908-
{RPCResult::Type::BOOL, "allowed", "If the mempool allows this tx to be inserted"},
914+
{RPCResult::Type::STR, "package-error", "Package validation error, if any (only possible if rawtxs had more than 1 transaction)."},
915+
{RPCResult::Type::BOOL, "allowed", "Whether this tx would be accepted to the mempool and pass client-specified maxfeerate."
916+
"If not present, the tx was not fully validated due to a failure in another tx in the list."},
909917
{RPCResult::Type::NUM, "vsize", "Virtual transaction size as defined in BIP 141. This is different from actual serialized size for witness transactions as witness data is discounted (only present when 'allowed' is true)"},
910918
{RPCResult::Type::OBJ, "fees", "Transaction fees (only present if 'allowed' is true)",
911919
{
@@ -932,62 +940,86 @@ static RPCHelpMan testmempoolaccept()
932940
UniValueType(), // VNUM or VSTR, checked inside AmountFromValue()
933941
});
934942

935-
if (request.params[0].get_array().size() != 1) {
936-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Array must contain exactly one raw transaction for now");
937-
}
938-
939-
CMutableTransaction mtx;
940-
if (!DecodeHexTx(mtx, request.params[0].get_array()[0].get_str())) {
941-
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed. Make sure the tx has at least one input.");
943+
const UniValue raw_transactions = request.params[0].get_array();
944+
if (raw_transactions.size() < 1 || raw_transactions.size() > MAX_PACKAGE_COUNT) {
945+
throw JSONRPCError(RPC_INVALID_PARAMETER,
946+
"Array must contain between 1 and " + ToString(MAX_PACKAGE_COUNT) + " transactions.");
942947
}
943-
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
944948

945949
const CFeeRate max_raw_tx_fee_rate = request.params[1].isNull() ?
946950
DEFAULT_MAX_RAW_TX_FEE_RATE :
947951
CFeeRate(AmountFromValue(request.params[1]));
948952

949-
NodeContext& node = EnsureAnyNodeContext(request.context);
953+
std::vector<CTransactionRef> txns;
954+
for (const auto& rawtx : raw_transactions.getValues()) {
955+
CMutableTransaction mtx;
956+
if (!DecodeHexTx(mtx, rawtx.get_str())) {
957+
throw JSONRPCError(RPC_DESERIALIZATION_ERROR,
958+
"TX decode failed: " + rawtx.get_str() + " Make sure the tx has at least one input.");
959+
}
960+
txns.emplace_back(MakeTransactionRef(std::move(mtx)));
961+
}
950962

963+
NodeContext& node = EnsureAnyNodeContext(request.context);
951964
CTxMemPool& mempool = EnsureMemPool(node);
952-
int64_t virtual_size = GetVirtualTransactionSize(*tx);
953-
CAmount max_raw_tx_fee = max_raw_tx_fee_rate.GetFee(virtual_size);
954-
955-
UniValue result(UniValue::VARR);
956-
UniValue result_0(UniValue::VOBJ);
957-
result_0.pushKV("txid", tx->GetHash().GetHex());
958-
result_0.pushKV("wtxid", tx->GetWitnessHash().GetHex());
959-
960-
ChainstateManager& chainman = EnsureChainman(node);
961-
const MempoolAcceptResult accept_result = WITH_LOCK(cs_main, return AcceptToMemoryPool(chainman.ActiveChainstate(), mempool, std::move(tx),
962-
false /* bypass_limits */, /* test_accept */ true));
963-
964-
// Only return the fee and vsize if the transaction would pass ATMP.
965-
// These can be used to calculate the feerate.
966-
if (accept_result.m_result_type == MempoolAcceptResult::ResultType::VALID) {
967-
const CAmount fee = accept_result.m_base_fees.value();
968-
// Check that fee does not exceed maximum fee
969-
if (max_raw_tx_fee && fee > max_raw_tx_fee) {
970-
result_0.pushKV("allowed", false);
971-
result_0.pushKV("reject-reason", "max-fee-exceeded");
972-
} else {
973-
result_0.pushKV("allowed", true);
974-
result_0.pushKV("vsize", virtual_size);
975-
UniValue fees(UniValue::VOBJ);
976-
fees.pushKV("base", ValueFromAmount(fee));
977-
result_0.pushKV("fees", fees);
965+
CChainState& chainstate = EnsureChainman(node).ActiveChainstate();
966+
const PackageMempoolAcceptResult package_result = [&] {
967+
LOCK(::cs_main);
968+
if (txns.size() > 1) return ProcessNewPackage(chainstate, mempool, txns, /* test_accept */ true);
969+
return PackageMempoolAcceptResult(txns[0]->GetWitnessHash(),
970+
AcceptToMemoryPool(chainstate, mempool, txns[0], /* bypass_limits */ false, /* test_accept*/ true));
971+
}();
972+
973+
UniValue rpc_result(UniValue::VARR);
974+
// We will check transaction fees we iterate through txns in order. If any transaction fee
975+
// exceeds maxfeerate, we will keave the rest of the validation results blank, because it
976+
// doesn't make sense to return a validation result for a transaction if its ancestor(s) would
977+
// not be submitted.
978+
bool exit_early{false};
979+
for (const auto& tx : txns) {
980+
UniValue result_inner(UniValue::VOBJ);
981+
result_inner.pushKV("txid", tx->GetHash().GetHex());
982+
result_inner.pushKV("wtxid", tx->GetWitnessHash().GetHex());
983+
if (package_result.m_state.GetResult() == PackageValidationResult::PCKG_POLICY) {
984+
result_inner.pushKV("package-error", package_result.m_state.GetRejectReason());
978985
}
979-
result.push_back(std::move(result_0));
980-
} else {
981-
result_0.pushKV("allowed", false);
982-
const TxValidationState state = accept_result.m_state;
983-
if (state.GetResult() == TxValidationResult::TX_MISSING_INPUTS) {
984-
result_0.pushKV("reject-reason", "missing-inputs");
986+
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
987+
if (exit_early || it == package_result.m_tx_results.end()) {
988+
// Validation unfinished. Just return the txid and wtxid.
989+
rpc_result.push_back(result_inner);
990+
continue;
991+
}
992+
const auto& tx_result = it->second;
993+
if (tx_result.m_result_type == MempoolAcceptResult::ResultType::VALID) {
994+
const CAmount fee = tx_result.m_base_fees.value();
995+
// Check that fee does not exceed maximum fee
996+
const int64_t virtual_size = GetVirtualTransactionSize(*tx);
997+
const CAmount max_raw_tx_fee = max_raw_tx_fee_rate.GetFee(virtual_size);
998+
if (max_raw_tx_fee && fee > max_raw_tx_fee) {
999+
result_inner.pushKV("allowed", false);
1000+
result_inner.pushKV("reject-reason", "max-fee-exceeded");
1001+
exit_early = true;
1002+
} else {
1003+
// Only return the fee and vsize if the transaction would pass ATMP.
1004+
// These can be used to calculate the feerate.
1005+
result_inner.pushKV("allowed", true);
1006+
result_inner.pushKV("vsize", virtual_size);
1007+
UniValue fees(UniValue::VOBJ);
1008+
fees.pushKV("base", ValueFromAmount(fee));
1009+
result_inner.pushKV("fees", fees);
1010+
}
9851011
} else {
986-
result_0.pushKV("reject-reason", state.GetRejectReason());
1012+
result_inner.pushKV("allowed", false);
1013+
const TxValidationState state = tx_result.m_state;
1014+
if (state.GetResult() == TxValidationResult::TX_MISSING_INPUTS) {
1015+
result_inner.pushKV("reject-reason", "missing-inputs");
1016+
} else {
1017+
result_inner.pushKV("reject-reason", state.GetRejectReason());
1018+
}
9871019
}
988-
result.push_back(std::move(result_0));
1020+
rpc_result.push_back(result_inner);
9891021
}
990-
return result;
1022+
return rpc_result;
9911023
},
9921024
};
9931025
}

src/test/fuzz/tx_pool.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,16 @@ FUZZ_TARGET_INIT(tx_pool_standard, initialize_tx_pool)
219219
RegisterSharedValidationInterface(txr);
220220
const bool bypass_limits = fuzzed_data_provider.ConsumeBool();
221221
::fRequireStandard = fuzzed_data_provider.ConsumeBool();
222+
223+
// Make sure ProcessNewPackage on one transaction works and always fully validates the transaction.
224+
// The result is not guaranteed to be the same as what is returned by ATMP.
225+
const auto result_package = WITH_LOCK(::cs_main,
226+
return ProcessNewPackage(node.chainman->ActiveChainstate(), tx_pool, {tx}, true));
227+
auto it = result_package.m_tx_results.find(tx->GetWitnessHash());
228+
Assert(it != result_package.m_tx_results.end());
229+
Assert(it->second.m_result_type == MempoolAcceptResult::ResultType::VALID ||
230+
it->second.m_result_type == MempoolAcceptResult::ResultType::INVALID);
231+
222232
const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, tx_pool, tx, bypass_limits));
223233
const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID;
224234
SyncWithValidationInterfaceQueue();

src/test/miner_tests.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ struct MinerTestingSetup : public TestingSetup {
2828
void TestPackageSelection(const CChainParams& chainparams, const CScript& scriptPubKey, const std::vector<CTransactionRef>& txFirst) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, m_node.mempool->cs);
2929
bool TestSequenceLocks(const CTransaction& tx, int flags) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, m_node.mempool->cs)
3030
{
31-
return CheckSequenceLocks(::ChainstateActive(), *m_node.mempool, tx, flags);
31+
CCoinsViewMemPool viewMempool(&m_node.chainman->ActiveChainstate().CoinsTip(), *m_node.mempool);
32+
return CheckSequenceLocks(m_node.chainman->ActiveChain().Tip(), viewMempool, tx, flags);
3233
}
3334
BlockAssembler AssemblerForTest(const CChainParams& params);
3435
};

0 commit comments

Comments
 (0)