diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -15,6 +15,12 @@ tracked by the node. - Added a new field `immature_stake_amount` to `getavalancheinfo` to report the total amount of stake that will mature within the next 2016 blocks. + - The `testmempoolaccept` RPC now accepts multiple transactions (still experimental at the moment, + API may be unstable). This is intended for testing transaction packages with dependency + relationships; it is not recommended for batch-validating independent transactions. In addition to + mempool policy, package policies apply: the list cannot contain more than 50 transactions or have a + total size exceeding 101K bytes, and cannot conflict with (spend the same inputs as) each other or + the mempool. P2P and network changes ----------------------- diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -1052,19 +1053,26 @@ static RPCHelpMan testmempoolaccept() { return RPCHelpMan{ "testmempoolaccept", - "Returns result of mempool acceptance tests indicating if raw" - " transaction (serialized, hex-encoded) would be accepted" - " by mempool.\n" - "\nThis checks if the transaction violates the consensus or policy " - "rules.\n" - "\nSee sendrawtransaction call.\n", + "\nReturns result of mempool acceptance tests indicating if raw " + "transaction(s) (serialized, hex-encoded) would be accepted by " + "mempool.\n" + "\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" + "\nIf one transaction fails, other transactions may not be fully " + "validated (the 'allowed' key will be blank).\n" + "\nThe maximum number of transactions allowed is " + + ToString(MAX_PACKAGE_COUNT) + + ".\n" + "\nThis checks if transactions violate the consensus or policy " + "rules.\n" + "\nSee sendrawtransaction call.\n", { { "rawtxs", RPCArg::Type::ARR, RPCArg::Optional::NO, - "An array of hex strings of raw transactions.\n" - " Length must be one for now.", + "An array of hex strings of raw transactions.", { {"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""}, @@ -1082,7 +1090,10 @@ "", "The result of the mempool acceptance test for each raw " "transaction in the input array.\n" - "Length is exactly one for now.", + "Returns results for each transaction in the same order they were " + "passed in.\n" + "It is possible for transactions to not be fully validated " + "('allowed' unset) if another transaction failed.\n", { {RPCResult::Type::OBJ, "", @@ -1090,8 +1101,14 @@ { {RPCResult::Type::STR_HEX, "txid", "The transaction hash in hex"}, + {RPCResult::Type::STR, "package-error", + "Package validation error, if any (only possible if " + "rawtxs had more than 1 transaction)."}, {RPCResult::Type::BOOL, "allowed", - "If the mempool allows this tx to be inserted"}, + "Whether this tx would be accepted to the mempool and " + "pass client-specified maxfeerate. " + "If not present, the tx was not fully validated due to a " + "failure in another tx in the list."}, {RPCResult::Type::NUM, "size", "The transaction size"}, {RPCResult::Type::OBJ, "fees", @@ -1125,73 +1142,107 @@ // VNUM or VSTR, checked inside AmountFromValue() UniValueType(), }); - - if (request.params[0].get_array().size() != 1) { - throw JSONRPCError( - RPC_INVALID_PARAMETER, - "Array must contain exactly one raw transaction for now"); - } - - CMutableTransaction mtx; - if (!DecodeHexTx(mtx, request.params[0].get_array()[0].get_str())) { - throw JSONRPCError(RPC_DESERIALIZATION_ERROR, - "TX decode failed"); + const UniValue raw_transactions = request.params[0].get_array(); + if (raw_transactions.size() < 1 || + raw_transactions.size() > MAX_PACKAGE_COUNT) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Array must contain between 1 and " + + ToString(MAX_PACKAGE_COUNT) + + " transactions."); } - CTransactionRef tx(MakeTransactionRef(std::move(mtx))); - const TxId &txid = tx->GetId(); const CFeeRate max_raw_tx_fee_rate = request.params[1].isNull() ? DEFAULT_MAX_RAW_TX_FEE_RATE : CFeeRate(AmountFromValue(request.params[1])); - NodeContext &node = EnsureAnyNodeContext(request.context); + std::vector txns; + txns.reserve(raw_transactions.size()); + for (const auto &rawtx : raw_transactions.getValues()) { + CMutableTransaction mtx; + if (!DecodeHexTx(mtx, rawtx.get_str())) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, + "TX decode failed: " + rawtx.get_str()); + } + txns.emplace_back(MakeTransactionRef(std::move(mtx))); + } + NodeContext &node = EnsureAnyNodeContext(request.context); CTxMemPool &mempool = EnsureMemPool(node); - int64_t virtual_size = GetVirtualTransactionSize(*tx); - Amount max_raw_tx_fee = max_raw_tx_fee_rate.GetFee(virtual_size); - - UniValue result(UniValue::VARR); - UniValue result_0(UniValue::VOBJ); - result_0.pushKV("txid", txid.GetHex()); - - ChainstateManager &chainman = EnsureChainman(node); - - const MempoolAcceptResult accept_result = WITH_LOCK( - cs_main, - return AcceptToMemoryPool( - chainman.ActiveChainstate(), config, mempool, std::move(tx), - false /* bypass_limits */, true /* test_accept */)); - - // Only return the fee and size if the transaction would pass ATMP. - // These can be used to calculate the feerate. - if (accept_result.m_result_type == - MempoolAcceptResult::ResultType::VALID) { - const Amount fee = accept_result.m_base_fees.value(); - // Check that fee does not exceed maximum fee - if (max_raw_tx_fee != Amount::zero() && fee > max_raw_tx_fee) { - result_0.pushKV("allowed", false); - result_0.pushKV("reject-reason", "max-fee-exceeded"); - } else { - result_0.pushKV("allowed", true); - result_0.pushKV("size", virtual_size); - UniValue fees(UniValue::VOBJ); - fees.pushKV("base", fee); - result_0.pushKV("fees", fees); + CChainState &chainstate = EnsureChainman(node).ActiveChainstate(); + const PackageMempoolAcceptResult package_result = [&] { + LOCK(::cs_main); + if (txns.size() > 1) { + return ProcessNewPackage(config, chainstate, mempool, txns, + /* test_accept */ true); } - result.push_back(std::move(result_0)); - } else { - result_0.pushKV("allowed", false); - const TxValidationState state = accept_result.m_state; - if (state.GetResult() == - TxValidationResult::TX_MISSING_INPUTS) { - result_0.pushKV("reject-reason", "missing-inputs"); + return PackageMempoolAcceptResult( + txns[0]->GetId(), + AcceptToMemoryPool(chainstate, config, mempool, txns[0], + /* bypass_limits */ false, + /* test_accept*/ true)); + }(); + + UniValue rpc_result(UniValue::VARR); + // We will check transaction fees while we iterate through txns in + // order. If any transaction fee exceeds maxfeerate, we will leave + // the rest of the validation results blank, because it doesn't make + // sense to return a validation result for a transaction if its + // ancestor(s) would not be submitted. + bool exit_early{false}; + for (const auto &tx : txns) { + UniValue result_inner(UniValue::VOBJ); + result_inner.pushKV("txid", tx->GetId().GetHex()); + if (package_result.m_state.GetResult() == + PackageValidationResult::PCKG_POLICY) { + result_inner.pushKV( + "package-error", + package_result.m_state.GetRejectReason()); + } + auto it = package_result.m_tx_results.find(tx->GetId()); + if (exit_early || it == package_result.m_tx_results.end()) { + // Validation unfinished. Just return the txid. + rpc_result.push_back(result_inner); + continue; + } + const auto &tx_result = it->second; + if (tx_result.m_result_type == + MempoolAcceptResult::ResultType::VALID) { + const Amount fee = tx_result.m_base_fees.value(); + // Check that fee does not exceed maximum fee + const int64_t virtual_size = GetVirtualTransactionSize(*tx); + const Amount max_raw_tx_fee = + max_raw_tx_fee_rate.GetFee(virtual_size); + if (max_raw_tx_fee != Amount::zero() && + fee > max_raw_tx_fee) { + result_inner.pushKV("allowed", false); + result_inner.pushKV("reject-reason", + "max-fee-exceeded"); + exit_early = true; + } else { + // Only return the fee and size if the transaction + // would pass ATMP. + // These can be used to calculate the feerate. + result_inner.pushKV("allowed", true); + result_inner.pushKV("size", virtual_size); + UniValue fees(UniValue::VOBJ); + fees.pushKV("base", fee); + result_inner.pushKV("fees", fees); + } } else { - result_0.pushKV("reject-reason", state.GetRejectReason()); + result_inner.pushKV("allowed", false); + const TxValidationState state = tx_result.m_state; + if (state.GetResult() == + TxValidationResult::TX_MISSING_INPUTS) { + result_inner.pushKV("reject-reason", "missing-inputs"); + } else { + result_inner.pushKV("reject-reason", + state.GetRejectReason()); + } } - result.push_back(std::move(result_0)); + rpc_result.push_back(result_inner); } - return result; + return rpc_result; }, }; } diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py --- a/test/functional/mempool_accept.py +++ b/test/functional/mempool_accept.py @@ -64,8 +64,10 @@ self.log.info('Should not accept garbage to testmempoolaccept') assert_raises_rpc_error(-3, 'Expected type array, got string', lambda: node.testmempoolaccept(rawtxs='ff00baar')) - assert_raises_rpc_error(-8, 'Array must contain exactly one raw transaction for now', - lambda: node.testmempoolaccept(rawtxs=['ff00baar', 'ff22'])) + assert_raises_rpc_error(-8, 'Array must contain between 1 and 50 transactions.', + lambda: node.testmempoolaccept(rawtxs=['ff22'] * 51)) + assert_raises_rpc_error(-8, 'Array must contain between 1 and 50 transactions.', + lambda: node.testmempoolaccept(rawtxs=[])) assert_raises_rpc_error(-22, 'TX decode failed', lambda: node.testmempoolaccept(rawtxs=['ff00baar']))