mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-28 02:32:14 +01:00
[rpc] add new submitpackage RPC
It could be unsafe/confusing to create an actual mainnet interface while package relay doesn't exist. However, a regtest-only interface allows wallet/application devs to test current package policies.
This commit is contained in:
parent
b9ef5a10e2
commit
fa076515b0
@ -110,6 +110,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||
{ "sendrawtransaction", 1, "maxfeerate" },
|
||||
{ "testmempoolaccept", 0, "rawtxs" },
|
||||
{ "testmempoolaccept", 1, "maxfeerate" },
|
||||
{ "submitpackage", 0, "package" },
|
||||
{ "combinerawtransaction", 0, "txs" },
|
||||
{ "fundrawtransaction", 1, "options" },
|
||||
{ "fundrawtransaction", 2, "iswitness" },
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
#include <rpc/blockchain.h>
|
||||
|
||||
#include <chainparams.h>
|
||||
#include <core_io.h>
|
||||
#include <fs.h>
|
||||
#include <policy/rbf.h>
|
||||
@ -729,6 +730,150 @@ static RPCHelpMan savemempool()
|
||||
};
|
||||
}
|
||||
|
||||
static RPCHelpMan submitpackage()
|
||||
{
|
||||
return RPCHelpMan{"submitpackage",
|
||||
"Submit a package of raw transactions (serialized, hex-encoded) to local node (-regtest only).\n"
|
||||
"The package will be validated according to consensus and mempool policy rules. If all transactions pass, they will be accepted to mempool.\n"
|
||||
"This RPC is experimental and the interface may be unstable. Refer to doc/policy/packages.md for documentation on package policies.\n"
|
||||
"Warning: until package relay is in use, successful submission does not mean the transaction will propagate to other nodes on the network.\n"
|
||||
"Currently, each transaction is broadcasted individually after submission, which means they must meet other nodes' feerate requirements alone.\n"
|
||||
,
|
||||
{
|
||||
{"package", RPCArg::Type::ARR, RPCArg::Optional::NO, "An array of raw transactions.",
|
||||
{
|
||||
{"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
RPCResult{
|
||||
RPCResult::Type::OBJ, "", "",
|
||||
{
|
||||
{RPCResult::Type::OBJ_DYN, "tx-results", "transaction results keyed by wtxid",
|
||||
{
|
||||
{RPCResult::Type::OBJ, "wtxid", "transaction wtxid", {
|
||||
{RPCResult::Type::STR_HEX, "txid", "The transaction hash in hex"},
|
||||
{RPCResult::Type::STR_HEX, "other-wtxid", /*optional=*/true, "The wtxid of a different transaction with the same txid but different witness found in the mempool. This means the submitted transaction was ignored."},
|
||||
{RPCResult::Type::NUM, "vsize", "Virtual transaction size as defined in BIP 141."},
|
||||
{RPCResult::Type::OBJ, "fees", "Transaction fees", {
|
||||
{RPCResult::Type::STR_AMOUNT, "base", "transaction fee in " + CURRENCY_UNIT},
|
||||
}},
|
||||
}}
|
||||
}},
|
||||
{RPCResult::Type::STR_AMOUNT, "package-feerate", /*optional=*/true, "package feerate used for feerate checks in " + CURRENCY_UNIT + " per KvB. Excludes transactions which were deduplicated or accepted individually."},
|
||||
{RPCResult::Type::ARR, "replaced-transactions", /*optional=*/true, "List of txids of replaced transactions",
|
||||
{
|
||||
{RPCResult::Type::STR_HEX, "", "The transaction id"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
RPCExamples{
|
||||
HelpExampleCli("testmempoolaccept", "[rawtx1, rawtx2]") +
|
||||
HelpExampleCli("submitpackage", "[rawtx1, rawtx2]")
|
||||
},
|
||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
if (!Params().IsMockableChain()) {
|
||||
throw std::runtime_error("submitpackage is for regression testing (-regtest mode) only");
|
||||
}
|
||||
RPCTypeCheck(request.params, {
|
||||
UniValue::VARR,
|
||||
});
|
||||
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.");
|
||||
}
|
||||
|
||||
std::vector<CTransactionRef> 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() + " Make sure the tx has at least one input.");
|
||||
}
|
||||
txns.emplace_back(MakeTransactionRef(std::move(mtx)));
|
||||
}
|
||||
|
||||
NodeContext& node = EnsureAnyNodeContext(request.context);
|
||||
CTxMemPool& mempool = EnsureMemPool(node);
|
||||
CChainState& chainstate = EnsureChainman(node).ActiveChainstate();
|
||||
const auto package_result = WITH_LOCK(::cs_main, return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/ false));
|
||||
|
||||
// First catch any errors.
|
||||
switch(package_result.m_state.GetResult()) {
|
||||
case PackageValidationResult::PCKG_RESULT_UNSET: break;
|
||||
case PackageValidationResult::PCKG_POLICY:
|
||||
{
|
||||
throw JSONRPCTransactionError(TransactionError::INVALID_PACKAGE,
|
||||
package_result.m_state.GetRejectReason());
|
||||
}
|
||||
case PackageValidationResult::PCKG_MEMPOOL_ERROR:
|
||||
{
|
||||
throw JSONRPCTransactionError(TransactionError::MEMPOOL_ERROR,
|
||||
package_result.m_state.GetRejectReason());
|
||||
}
|
||||
case PackageValidationResult::PCKG_TX:
|
||||
{
|
||||
for (const auto& tx : txns) {
|
||||
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
|
||||
if (it != package_result.m_tx_results.end() && it->second.m_state.IsInvalid()) {
|
||||
throw JSONRPCTransactionError(TransactionError::MEMPOOL_REJECTED,
|
||||
strprintf("%s failed: %s", tx->GetHash().ToString(), it->second.m_state.GetRejectReason()));
|
||||
}
|
||||
}
|
||||
// If a PCKG_TX error was returned, there must have been an invalid transaction.
|
||||
NONFATAL_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
for (const auto& tx : txns) {
|
||||
size_t num_submitted{0};
|
||||
std::string err_string;
|
||||
const auto err = BroadcastTransaction(node, tx, err_string, 0, true, true);
|
||||
if (err != TransactionError::OK) {
|
||||
throw JSONRPCTransactionError(err,
|
||||
strprintf("transaction broadcast failed: %s (all transactions were submitted, %d transactions were broadcast successfully)",
|
||||
err_string, num_submitted));
|
||||
}
|
||||
}
|
||||
UniValue rpc_result{UniValue::VOBJ};
|
||||
UniValue tx_result_map{UniValue::VOBJ};
|
||||
std::set<uint256> replaced_txids;
|
||||
for (const auto& tx : txns) {
|
||||
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
|
||||
CHECK_NONFATAL(it != package_result.m_tx_results.end());
|
||||
UniValue result_inner{UniValue::VOBJ};
|
||||
result_inner.pushKV("txid", tx->GetHash().GetHex());
|
||||
if (it->second.m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS) {
|
||||
result_inner.pushKV("other-wtxid", it->second.m_other_wtxid.value().GetHex());
|
||||
}
|
||||
if (it->second.m_result_type == MempoolAcceptResult::ResultType::VALID ||
|
||||
it->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY) {
|
||||
result_inner.pushKV("vsize", int64_t{it->second.m_vsize.value()});
|
||||
UniValue fees(UniValue::VOBJ);
|
||||
fees.pushKV("base", ValueFromAmount(it->second.m_base_fees.value()));
|
||||
result_inner.pushKV("fees", fees);
|
||||
if (it->second.m_replaced_transactions.has_value()) {
|
||||
for (const auto& ptx : it->second.m_replaced_transactions.value()) {
|
||||
replaced_txids.insert(ptx->GetHash());
|
||||
}
|
||||
}
|
||||
}
|
||||
tx_result_map.pushKV(tx->GetWitnessHash().GetHex(), result_inner);
|
||||
}
|
||||
rpc_result.pushKV("tx-results", tx_result_map);
|
||||
if (package_result.m_package_feerate.has_value()) {
|
||||
rpc_result.pushKV("package-feerate", ValueFromAmount(package_result.m_package_feerate.value().GetFeePerK()));
|
||||
}
|
||||
UniValue replaced_list(UniValue::VARR);
|
||||
for (const uint256& hash : replaced_txids) replaced_list.push_back(hash.ToString());
|
||||
rpc_result.pushKV("replaced-transactions", replaced_list);
|
||||
return rpc_result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
void RegisterMempoolRPCCommands(CRPCTable& t)
|
||||
{
|
||||
static const CRPCCommand commands[]{
|
||||
@ -741,6 +886,7 @@ void RegisterMempoolRPCCommands(CRPCTable& t)
|
||||
{"blockchain", &getmempoolinfo},
|
||||
{"blockchain", &getrawmempool},
|
||||
{"blockchain", &savemempool},
|
||||
{"hidden", &submitpackage},
|
||||
};
|
||||
for (const auto& c : commands) {
|
||||
t.appendCommand(c.name, &c);
|
||||
|
@ -159,6 +159,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
|
||||
"signrawtransactionwithkey",
|
||||
"submitblock",
|
||||
"submitheader",
|
||||
"submitpackage",
|
||||
"syncwithvalidationinterfacequeue",
|
||||
"testmempoolaccept",
|
||||
"uptime",
|
||||
|
@ -35,6 +35,8 @@ bilingual_str TransactionErrorString(const TransactionError err)
|
||||
return Untranslated("External signer not found");
|
||||
case TransactionError::EXTERNAL_SIGNER_FAILED:
|
||||
return Untranslated("External signer failed to sign");
|
||||
case TransactionError::INVALID_PACKAGE:
|
||||
return Untranslated("Transaction rejected due to invalid package");
|
||||
// no default case, so the compiler can warn about missing cases
|
||||
}
|
||||
assert(false);
|
||||
|
@ -32,6 +32,7 @@ enum class TransactionError {
|
||||
MAX_FEE_EXCEEDED,
|
||||
EXTERNAL_SIGNER_NOT_FOUND,
|
||||
EXTERNAL_SIGNER_FAILED,
|
||||
INVALID_PACKAGE,
|
||||
};
|
||||
|
||||
bilingual_str TransactionErrorString(const TransactionError error);
|
||||
|
Loading…
x
Reference in New Issue
Block a user