Skip to content

Commit

Permalink
Merge bitcoin#24836: add RPC (-regtest only) for testing package policy
Browse files Browse the repository at this point in the history
e866f0d [functional test] submitrawpackage RPC (glozow)
fa07651 [rpc] add new submitpackage RPC (glozow)

Pull request description:

  It would be nice for LN/wallet/app devs to test out package policy, package RBF, etc., but the only interface to do so right now is through unit tests. This PR adds a `-regtest` only RPC interface so people can test by submitting raw transaction data. It is regtest-only, as it would be unsafe/confusing to create an actual mainnet interface while package relay doesn't exist.

  Note that the functional tests are there to ensure the RPC interface is working properly; they aren't for testing policy itself. See src/test/txpackage_tests.cpp.

ACKs for top commit:
  t-bast:
    Tested ACK against eclair bitcoin@e866f0d
  ariard:
    Code Review ACK e866f0d
  instagibbs:
    code review ACK e866f0d

Tree-SHA512: 824a26b10d2240e0fd85e5dd25bf499ee3dd9ba8ef4f522533998fcf767ddded9f001f7a005fe3ab07ec95e696448484e26599803e6034ed2733125c8c376c84
  • Loading branch information
fanquake authored and vijaydasmp committed Jan 9, 2025
1 parent 9a96514 commit 3941a52
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 11 deletions.
21 changes: 15 additions & 6 deletions src/net_processing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ class PeerManagerImpl final : public PeerManager
void RelayRecoveredSig(const uint256& sigHash) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void RelayDSQ(const CCoinJoinQueue& queue) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void SetBestHeight(int height) override { m_best_height = height; };
void UnitTestMisbehaving(NodeId peer_id, const int howmuch, const std::string& message = "") override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex) { Misbehaving(*Assert(GetPeerRef(peer_id)), howmuch, ""); };
void UnitTestMisbehaving(NodeId peer_id, const int howmuch) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex) { Misbehaving(*Assert(GetPeerRef(peer_id)), howmuch, ""); };
void ProcessMessage(CNode& pfrom, const std::string& msg_type, CDataStream& vRecv,
const std::chrono::microseconds time_received, const std::atomic<bool>& interruptMsgProc) override
EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !m_recent_confirmed_transactions_mutex, g_msgproc_mutex);
Expand Down Expand Up @@ -3480,8 +3480,19 @@ void PeerManagerImpl::ProcessBlock(CNode& node, const std::shared_ptr<const CBlo

void PeerManagerImpl::ProcessPeerMsgRet(const PeerMsgRet& ret, CNode& pfrom)
{
<<<<<<< HEAD
<<<<<<< HEAD
PeerRef peer{GetPeerRef(pfrom.GetId()}
=======
PeerRef peer{GetPeerRef(pfrom.GetId())}
>>>>>>> 7d382c7c00... 25144
if (peer && !ret) Misbehaving(*peer, ret.error().score, ret.error().message);
=======
PeerRef peer{GetPeerRef(pfrom.GetId())}
if (peer && !ret) {
Misbehaving(*peer, ret.error().score, ret.error().message);
}
>>>>>>> bec9213d9f... 25144
}

void PeerManagerImpl::PostProcessMessage(MessageProcessingResult&& result, NodeId node)
Expand Down Expand Up @@ -3614,13 +3625,12 @@ void PeerManagerImpl::ProcessMessage(
}

if (Params().NetworkIDString() == CBaseChainParams::DEVNET) {
PeerRef peer{GetPeerRef(pfrom.GetId())};
if (cleanSubVer.find(strprintf("devnet.%s", gArgs.GetDevNetName())) == std::string::npos) {
LogPrintf("connected to wrong devnet. Reported version is %s, expected devnet name is %s\n", cleanSubVer, gArgs.GetDevNetName());
if (peer && !pfrom.IsInboundConn())
Misbehaving(*peer, 100); // don't try to connect again
if (!pfrom.IsInboundConn())
UnitTestMisbehaving(pfrom.GetId(), 100); // don't try to connect again
else
Misbehaving(*peer, 1); // whover connected, might just have made a mistake, don't ban him immediately
UnitTestMisbehaving(pfrom.GetId(), 1); // whover connected, might just have made a mistake, don't ban him immediately
pfrom.fDisconnect = true;
return;
}
Expand Down Expand Up @@ -5197,7 +5207,6 @@ void PeerManagerImpl::ProcessMessage(
return;
}

PeerRef peer{GetPeerRef(pfrom.GetId())};
if (msg_type == NetMsgType::MNLISTDIFF) {
// we have never requested this
if (peer) Misbehaving(*peer, 100, strprintf("received not-requested mnlistdiff. peer=%d", pfrom.GetId()));
Expand Down
2 changes: 1 addition & 1 deletion src/net_processing.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class PeerManager : public CValidationInterface, public NetEventsInterface
virtual void SetBestHeight(int height) = 0;

/* Public for unit testing. */
virtual void UnitTestMisbehaving(NodeId peer_id, int howmuch, const std::string& message = "") = 0;
virtual void UnitTestMisbehaving(NodeId peer_id, int howmuch) = 0;

/**
* Evict extra outbound peers. If we think our tip may be stale, connect to an extra outbound.
Expand Down
1 change: 1 addition & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendrawtransaction", 3, "bypasslimits" },
{ "testmempoolaccept", 0, "rawtxs" },
{ "testmempoolaccept", 1, "maxfeerate" },
{ "submitpackage", 0, "package" },
{ "combinerawtransaction", 0, "txs" },
{ "fundrawtransaction", 1, "options" },
{ "walletcreatefundedpsbt", 0, "inputs" },
Expand Down
146 changes: 146 additions & 0 deletions src/rpc/mempool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <rpc/blockchain.h>

#include <chainparams.h>
#include <core_io.h>
#include <fs.h>
#include <policy/settings.h>
Expand Down Expand Up @@ -465,6 +466,150 @@ 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[]{
Expand All @@ -476,6 +621,7 @@ void RegisterMempoolRPCCommands(CRPCTable& t)
{"blockchain", &getmempoolinfo},
{"blockchain", &getrawmempool},
{"blockchain", &savemempool},
{"hidden", &submitpackage},
};
for (const auto& c : commands) {
t.appendCommand(c.name, &c);
Expand Down
1 change: 1 addition & 0 deletions src/test/fuzz/rpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"signrawtransactionwithkey",
"submitblock",
"submitheader",
"submitpackage",
"syncwithvalidationinterfacequeue",
"testmempoolaccept",
"uptime",
Expand Down
2 changes: 2 additions & 0 deletions src/util/error.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ bilingual_str TransactionErrorString(const TransactionError err)
return Untranslated("Specified sighash value does not match value stored in PSBT");
case TransactionError::MAX_FEE_EXCEEDED:
return Untranslated("Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)");
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);
Expand Down
1 change: 1 addition & 0 deletions src/util/error.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ enum class TransactionError {
PSBT_MISMATCH,
SIGHASH_MISMATCH,
MAX_FEE_EXCEEDED,
INVALID_PACKAGE,
};

bilingual_str TransactionErrorString(const TransactionError error);
Expand Down
Loading

0 comments on commit 3941a52

Please sign in to comment.