Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/interfaces/chain.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ class Chain
public:
virtual ~Chain() = default;

// This method will remove the specified transaction and all of its descendants
// (transactions that spend outputs from this transaction or its descendants)
// from the mempool.
virtual bool removeTxFromMempool(const uint256& txid) = 0;

//! Get current chain height, not including genesis block (returns 0 if
//! chain only contains genesis block, nullopt if chain does not contain
//! any blocks)
Expand Down
8 changes: 4 additions & 4 deletions src/ipc/capnp/chain.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ interface Chain $Proxy.wrap("interfaces::Chain") {
deleteRwSettings @51 (context :Proxy.Context, name :Text, action: Int32) -> (result :Bool);
requestMempoolTransactions @52 (context :Proxy.Context, notifications :ChainNotifications) -> ();
hasAssumedValidChain @53 (context :Proxy.Context) -> (result :Bool);
removeTxFromMempool @54 (context :Proxy.Context, txid :Data) -> (result :Bool);
}

interface ChainNotifications $Proxy.wrap("interfaces::Chain::Notifications") {
Expand All @@ -88,10 +89,9 @@ interface ChainClient $Proxy.wrap("interfaces::ChainClient") {
verify @2 (context :Proxy.Context) -> (result :Bool);
load @3 (context :Proxy.Context) -> (result :Bool);
start @4 (context :Proxy.Context, scheduler :Void) -> ();
flush @5 (context :Proxy.Context) -> ();
stop @6 (context :Proxy.Context) -> ();
setMockTime @7 (context :Proxy.Context, time :Int64) -> ();
schedulerMockForward @8 (context :Proxy.Context, time :Int64) -> ();
stop @5 (context :Proxy.Context) -> ();
setMockTime @6 (context :Proxy.Context, time :Int64) -> ();
schedulerMockForward @7 (context :Proxy.Context, time :Int64) -> ();
}

struct FeeCalculation $Proxy.wrap("FeeCalculation") {
Expand Down
1 change: 1 addition & 0 deletions src/kernel/mempool_removal_reason.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ std::string RemovalReasonToString(const MemPoolRemovalReason& r) noexcept
case MemPoolRemovalReason::BLOCK: return "block";
case MemPoolRemovalReason::CONFLICT: return "conflict";
case MemPoolRemovalReason::REPLACED: return "replaced";
case MemPoolRemovalReason::MANUAL: return "manual";
}
assert(false);
}
1 change: 1 addition & 0 deletions src/kernel/mempool_removal_reason.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ enum class MemPoolRemovalReason {
BLOCK, //!< Removed for block
CONFLICT, //!< Removed for conflict with in-block transaction
REPLACED, //!< Removed for replacement
MANUAL, //!< Manually removed
};

std::string RemovalReasonToString(const MemPoolRemovalReason& r) noexcept;
Expand Down
13 changes: 13 additions & 0 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,19 @@ class ChainImpl : public Chain
LOCK(::cs_main);
return Assert(chainman().ActiveChain()[height])->GetBlockHash();
}
bool removeTxFromMempool(const uint256& txid) override
{
NodeContext& node = m_node;
if (!node.mempool) return false;
CTxMemPool& mempool = *node.mempool;
LOCK(mempool.cs);
CTransactionRef tx = mempool.get(txid);
if (tx) {
mempool.removeRecursive(*tx, MemPoolRemovalReason::MANUAL);
return true;
}
return false;
}
bool haveBlockOnDisk(int height) override
{
LOCK(::cs_main);
Expand Down
44 changes: 44 additions & 0 deletions src/rpc/mempool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,49 @@ static RPCHelpMan testmempoolaccept()
};
}


static RPCHelpMan removetxfrommempool()
{
return RPCHelpMan{"removetxfrommempool",
"Removes a transaction from the mempool.\n"
"This can be used for testing purposes, removing stuck transactions, or clearing conflicting transactions.\n"
"\nWarning: This will remove the specified transaction AND all of its descendants from the mempool.\n"
"Descendants are transactions that spend outputs from this transaction or any of its descendants.\n",
{
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id to remove from the mempool"},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::BOOL, "removed", "True if the transaction was in the mempool and was successfully removed"},
}
},
RPCExamples{
HelpExampleCli("removetxfrommempool", "\"mytxid\"")
+ HelpExampleRpc("removetxfrommempool", "\"mytxid\"")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
uint256 hash = ParseHashV(request.params[0], "txid");
NodeContext& node = EnsureAnyNodeContext(request.context);
CTxMemPool& mempool = EnsureMemPool(node);
UniValue result(UniValue::VOBJ);
bool removed = false;
{
LOCK(mempool.cs);
CTransactionRef tx = mempool.get(hash);
if (tx) {
// Transaction found in mempool, remove it and its descendants
mempool.removeRecursive(*tx, MemPoolRemovalReason::MANUAL);
removed = true;
}
}
result.pushKV("removed", removed);
return result;
},
};
}

static std::vector<RPCResult> MempoolEntryDescription()
{
return {
Expand Down Expand Up @@ -1146,6 +1189,7 @@ void RegisterMempoolRPCCommands(CRPCTable& t)
{"blockchain", &savemempool},
{"hidden", &getorphantxs},
{"rawtransactions", &submitpackage},
{"blockchain", &removetxfrommempool},
};
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 @@ -163,6 +163,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"prioritisetransaction",
"pruneblockchain",
"reconsiderblock",
"removetxfrommempool",
"scanblocks",
"scantxoutset",
"sendmsgtopeer", // when no peers are connected, no p2p message is sent
Expand Down
113 changes: 113 additions & 0 deletions test/functional/rpc_mempool_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,117 @@ class RPCMempoolInfoTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1

def test_removetxfrommempool(self):
"""Test for removetxfrommempool RPC functionality"""
self.log.info("Starting removetxfrommempool tests")
node = self.nodes[0]
initial_mempool_size = len(node.getrawmempool())

# Basic transaction removal
tx1 = self.wallet.send_self_transfer(from_node=node)
txid1 = tx1["txid"]
assert txid1 in node.getrawmempool()
assert_equal(len(node.getrawmempool()), initial_mempool_size + 1)
result = node.removetxfrommempool(txid1)
assert_equal(result["removed"], True)

# Verify transaction is no longer in mempool
assert txid1 not in node.getrawmempool()
assert_equal(len(node.getrawmempool()), initial_mempool_size)

# Removing non-existent transaction
fake_txid = "0" * 64
result = node.removetxfrommempool(fake_txid)
assert_equal(result["removed"], False)

# Recursive removal (parent-child transactions)
# Create parent transaction
parent_tx = self.wallet.send_self_transfer(from_node=node)
parent_txid = parent_tx["txid"]

# Create child transaction spending from parent
child_tx = self.wallet.send_self_transfer(
from_node=node,
utxo_to_spend=parent_tx["new_utxo"]
)
child_txid = child_tx["txid"]
mempool = node.getrawmempool()
assert parent_txid in mempool
assert child_txid in mempool
assert_equal(len(mempool), initial_mempool_size + 2)

# Remove parent transaction
result = node.removetxfrommempool(parent_txid)
assert_equal(result["removed"], True)

# Verify both parent and child are removed
mempool = node.getrawmempool()
assert parent_txid not in mempool
assert child_txid not in mempool
assert_equal(len(mempool), initial_mempool_size)

# Transaction chain removal
# Create a chain of 3 transactions: tx1 -> tx2 -> tx3
chain_tx1 = self.wallet.send_self_transfer(from_node=node)
chain_tx2 = self.wallet.send_self_transfer(
from_node=node,
utxo_to_spend=chain_tx1["new_utxo"]
)
chain_tx3 = self.wallet.send_self_transfer(
from_node=node,
utxo_to_spend=chain_tx2["new_utxo"]
)

# Verify all transactions are in mempool
mempool = node.getrawmempool()
assert_equal(len(mempool), initial_mempool_size + 3)
for tx in [chain_tx1, chain_tx2, chain_tx3]:
assert tx["txid"] in mempool

# Remove the middle transaction (chain_tx2)
result = node.removetxfrommempool(chain_tx2["txid"])
assert_equal(result["removed"], True)

# chain_tx2 and chain_tx3 should be removed, chain_tx1 should remain
mempool = node.getrawmempool()
assert chain_tx1["txid"] in mempool
assert chain_tx2["txid"] not in mempool
assert chain_tx3["txid"] not in mempool
assert_equal(len(mempool), initial_mempool_size + 1)

# Test with invalid txid format
assert_raises_rpc_error(-8, "txid must be of length 64", node.removetxfrommempool, "invalid")

# Test with wrong length hex (too short)
assert_raises_rpc_error(-8, "txid must be of length 64", node.removetxfrommempool, "a" * 63)

# Test with wrong length hex (too long)
assert_raises_rpc_error(-8, "txid must be of length 64", node.removetxfrommempool, "a" * 65)

# Test with no parameters
assert_raises_rpc_error(-1, "", node.removetxfrommempool)

# Test with valid hex but non-existent txid
valid_but_fake_txid = "a" * 64
result = node.removetxfrommempool(valid_but_fake_txid)
assert_equal(result["removed"], False)

# Test removing the same transaction twice
double_remove_tx = self.wallet.send_self_transfer(from_node=node)
double_remove_txid = double_remove_tx["txid"]
result1 = node.removetxfrommempool(double_remove_txid)
assert_equal(result1["removed"], True)
result2 = node.removetxfrommempool(double_remove_txid)
assert_equal(result2["removed"], False)

# Clean up remaining transactions
node.removetxfrommempool(chain_tx1["txid"])

# Verify mempool is clean
assert_equal(len(node.getrawmempool()), initial_mempool_size)

self.log.info("All removetxfrommempool tests completed successfully")

def run_test(self):
self.wallet = MiniWallet(self.nodes[0])
confirmed_utxo = self.wallet.get_utxo()
Expand Down Expand Up @@ -94,6 +205,8 @@ def create_tx(**kwargs):
self.log.info("Missing txid")
assert_raises_rpc_error(-3, "Missing txid", self.nodes[0].gettxspendingprevout, [{'vout' : 3}])

self.log.info("Test removetxfrommempool RPC")
self.test_removetxfrommempool()

if __name__ == '__main__':
RPCMempoolInfoTest(__file__).main()
Loading