Skip to content

Commit fa07651

Browse files
committed
[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.
1 parent b9ef5a1 commit fa07651

File tree

5 files changed

+151
-0
lines changed

5 files changed

+151
-0
lines changed

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
110110
{ "sendrawtransaction", 1, "maxfeerate" },
111111
{ "testmempoolaccept", 0, "rawtxs" },
112112
{ "testmempoolaccept", 1, "maxfeerate" },
113+
{ "submitpackage", 0, "package" },
113114
{ "combinerawtransaction", 0, "txs" },
114115
{ "fundrawtransaction", 1, "options" },
115116
{ "fundrawtransaction", 2, "iswitness" },

src/rpc/mempool.cpp

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <rpc/blockchain.h>
77

8+
#include <chainparams.h>
89
#include <core_io.h>
910
#include <fs.h>
1011
#include <policy/rbf.h>
@@ -729,6 +730,150 @@ static RPCHelpMan savemempool()
729730
};
730731
}
731732

733+
static RPCHelpMan submitpackage()
734+
{
735+
return RPCHelpMan{"submitpackage",
736+
"Submit a package of raw transactions (serialized, hex-encoded) to local node (-regtest only).\n"
737+
"The package will be validated according to consensus and mempool policy rules. If all transactions pass, they will be accepted to mempool.\n"
738+
"This RPC is experimental and the interface may be unstable. Refer to doc/policy/packages.md for documentation on package policies.\n"
739+
"Warning: until package relay is in use, successful submission does not mean the transaction will propagate to other nodes on the network.\n"
740+
"Currently, each transaction is broadcasted individually after submission, which means they must meet other nodes' feerate requirements alone.\n"
741+
,
742+
{
743+
{"package", RPCArg::Type::ARR, RPCArg::Optional::NO, "An array of raw transactions.",
744+
{
745+
{"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""},
746+
},
747+
},
748+
},
749+
RPCResult{
750+
RPCResult::Type::OBJ, "", "",
751+
{
752+
{RPCResult::Type::OBJ_DYN, "tx-results", "transaction results keyed by wtxid",
753+
{
754+
{RPCResult::Type::OBJ, "wtxid", "transaction wtxid", {
755+
{RPCResult::Type::STR_HEX, "txid", "The transaction hash in hex"},
756+
{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."},
757+
{RPCResult::Type::NUM, "vsize", "Virtual transaction size as defined in BIP 141."},
758+
{RPCResult::Type::OBJ, "fees", "Transaction fees", {
759+
{RPCResult::Type::STR_AMOUNT, "base", "transaction fee in " + CURRENCY_UNIT},
760+
}},
761+
}}
762+
}},
763+
{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."},
764+
{RPCResult::Type::ARR, "replaced-transactions", /*optional=*/true, "List of txids of replaced transactions",
765+
{
766+
{RPCResult::Type::STR_HEX, "", "The transaction id"},
767+
}},
768+
},
769+
},
770+
RPCExamples{
771+
HelpExampleCli("testmempoolaccept", "[rawtx1, rawtx2]") +
772+
HelpExampleCli("submitpackage", "[rawtx1, rawtx2]")
773+
},
774+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
775+
{
776+
if (!Params().IsMockableChain()) {
777+
throw std::runtime_error("submitpackage is for regression testing (-regtest mode) only");
778+
}
779+
RPCTypeCheck(request.params, {
780+
UniValue::VARR,
781+
});
782+
const UniValue raw_transactions = request.params[0].get_array();
783+
if (raw_transactions.size() < 1 || raw_transactions.size() > MAX_PACKAGE_COUNT) {
784+
throw JSONRPCError(RPC_INVALID_PARAMETER,
785+
"Array must contain between 1 and " + ToString(MAX_PACKAGE_COUNT) + " transactions.");
786+
}
787+
788+
std::vector<CTransactionRef> txns;
789+
txns.reserve(raw_transactions.size());
790+
for (const auto& rawtx : raw_transactions.getValues()) {
791+
CMutableTransaction mtx;
792+
if (!DecodeHexTx(mtx, rawtx.get_str())) {
793+
throw JSONRPCError(RPC_DESERIALIZATION_ERROR,
794+
"TX decode failed: " + rawtx.get_str() + " Make sure the tx has at least one input.");
795+
}
796+
txns.emplace_back(MakeTransactionRef(std::move(mtx)));
797+
}
798+
799+
NodeContext& node = EnsureAnyNodeContext(request.context);
800+
CTxMemPool& mempool = EnsureMemPool(node);
801+
CChainState& chainstate = EnsureChainman(node).ActiveChainstate();
802+
const auto package_result = WITH_LOCK(::cs_main, return ProcessNewPackage(chainstate, mempool, txns, /*test_accept=*/ false));
803+
804+
// First catch any errors.
805+
switch(package_result.m_state.GetResult()) {
806+
case PackageValidationResult::PCKG_RESULT_UNSET: break;
807+
case PackageValidationResult::PCKG_POLICY:
808+
{
809+
throw JSONRPCTransactionError(TransactionError::INVALID_PACKAGE,
810+
package_result.m_state.GetRejectReason());
811+
}
812+
case PackageValidationResult::PCKG_MEMPOOL_ERROR:
813+
{
814+
throw JSONRPCTransactionError(TransactionError::MEMPOOL_ERROR,
815+
package_result.m_state.GetRejectReason());
816+
}
817+
case PackageValidationResult::PCKG_TX:
818+
{
819+
for (const auto& tx : txns) {
820+
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
821+
if (it != package_result.m_tx_results.end() && it->second.m_state.IsInvalid()) {
822+
throw JSONRPCTransactionError(TransactionError::MEMPOOL_REJECTED,
823+
strprintf("%s failed: %s", tx->GetHash().ToString(), it->second.m_state.GetRejectReason()));
824+
}
825+
}
826+
// If a PCKG_TX error was returned, there must have been an invalid transaction.
827+
NONFATAL_UNREACHABLE();
828+
}
829+
}
830+
for (const auto& tx : txns) {
831+
size_t num_submitted{0};
832+
std::string err_string;
833+
const auto err = BroadcastTransaction(node, tx, err_string, 0, true, true);
834+
if (err != TransactionError::OK) {
835+
throw JSONRPCTransactionError(err,
836+
strprintf("transaction broadcast failed: %s (all transactions were submitted, %d transactions were broadcast successfully)",
837+
err_string, num_submitted));
838+
}
839+
}
840+
UniValue rpc_result{UniValue::VOBJ};
841+
UniValue tx_result_map{UniValue::VOBJ};
842+
std::set<uint256> replaced_txids;
843+
for (const auto& tx : txns) {
844+
auto it = package_result.m_tx_results.find(tx->GetWitnessHash());
845+
CHECK_NONFATAL(it != package_result.m_tx_results.end());
846+
UniValue result_inner{UniValue::VOBJ};
847+
result_inner.pushKV("txid", tx->GetHash().GetHex());
848+
if (it->second.m_result_type == MempoolAcceptResult::ResultType::DIFFERENT_WITNESS) {
849+
result_inner.pushKV("other-wtxid", it->second.m_other_wtxid.value().GetHex());
850+
}
851+
if (it->second.m_result_type == MempoolAcceptResult::ResultType::VALID ||
852+
it->second.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY) {
853+
result_inner.pushKV("vsize", int64_t{it->second.m_vsize.value()});
854+
UniValue fees(UniValue::VOBJ);
855+
fees.pushKV("base", ValueFromAmount(it->second.m_base_fees.value()));
856+
result_inner.pushKV("fees", fees);
857+
if (it->second.m_replaced_transactions.has_value()) {
858+
for (const auto& ptx : it->second.m_replaced_transactions.value()) {
859+
replaced_txids.insert(ptx->GetHash());
860+
}
861+
}
862+
}
863+
tx_result_map.pushKV(tx->GetWitnessHash().GetHex(), result_inner);
864+
}
865+
rpc_result.pushKV("tx-results", tx_result_map);
866+
if (package_result.m_package_feerate.has_value()) {
867+
rpc_result.pushKV("package-feerate", ValueFromAmount(package_result.m_package_feerate.value().GetFeePerK()));
868+
}
869+
UniValue replaced_list(UniValue::VARR);
870+
for (const uint256& hash : replaced_txids) replaced_list.push_back(hash.ToString());
871+
rpc_result.pushKV("replaced-transactions", replaced_list);
872+
return rpc_result;
873+
},
874+
};
875+
}
876+
732877
void RegisterMempoolRPCCommands(CRPCTable& t)
733878
{
734879
static const CRPCCommand commands[]{
@@ -741,6 +886,7 @@ void RegisterMempoolRPCCommands(CRPCTable& t)
741886
{"blockchain", &getmempoolinfo},
742887
{"blockchain", &getrawmempool},
743888
{"blockchain", &savemempool},
889+
{"hidden", &submitpackage},
744890
};
745891
for (const auto& c : commands) {
746892
t.appendCommand(c.name, &c);

src/test/fuzz/rpc.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
159159
"signrawtransactionwithkey",
160160
"submitblock",
161161
"submitheader",
162+
"submitpackage",
162163
"syncwithvalidationinterfacequeue",
163164
"testmempoolaccept",
164165
"uptime",

src/util/error.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ bilingual_str TransactionErrorString(const TransactionError err)
3535
return Untranslated("External signer not found");
3636
case TransactionError::EXTERNAL_SIGNER_FAILED:
3737
return Untranslated("External signer failed to sign");
38+
case TransactionError::INVALID_PACKAGE:
39+
return Untranslated("Transaction rejected due to invalid package");
3840
// no default case, so the compiler can warn about missing cases
3941
}
4042
assert(false);

src/util/error.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ enum class TransactionError {
3232
MAX_FEE_EXCEEDED,
3333
EXTERNAL_SIGNER_NOT_FOUND,
3434
EXTERNAL_SIGNER_FAILED,
35+
INVALID_PACKAGE,
3536
};
3637

3738
bilingual_str TransactionErrorString(const TransactionError error);

0 commit comments

Comments
 (0)