mining: add getTransactionsByTxID() IPC method
What changed, and why it matters
This commit adds a new inter-process communication (IPC) method that lets an external mining process ask the Bitcoin Core node to look up raw transactions in the memory pool by their transaction IDs. It is a straightforward read-only lookup: found transactions are returned, missing ones come back as empty/null. There is no obvious security flaw in the change itself, but it does slightly expand what the IPC mining interface can access.
Treat as a routine feature addition. Reviewers should confirm the IPC mining endpoint is appropriately authenticated/authorized in the broader IPC framework, and consider whether batch lookups need rate limiting or maximum request size bounds to avoid mempool-scanning denial of service, though none is evident in this commit.
Security signals we found
New IPC surface added to mining interface
Read-only mempool access by txid
Mempool lock (cs) held during batch lookup
Missing transactions returned as null/empty rather than error
No input validation or rate limiting visible in the diff
Evidence from the diff
The patch introduces Mining::getTransactionsByTxID() in src/interfaces/mining.h, exposes it through the Cap’n Proto IPC proxy in src/ipc/capnp/mining.capnp, and implements it in src/node/interfaces.cpp by locking the mempool and returning CTransactionRefs for each requested txid. Unit and functional tests verify that existing mempool transactions are returned, non-existing txids return null/empty, and mined (non-mempool) transactions are not returned. A small refactor in test_framework/messages.py adds a txid property reused by txid_hex and txid_int.
Changed components
src/interfaces/mining.hsrc/ipc/capnp/mining.capnpsrc/node/interfaces.cppsrc/test/miner_tests.cpptest/functional/interface_ipc_mining.pytest/functional/test_framework/messages.pyInspect captured patch +65 / −2
diff --git a/src/interfaces/mining.h b/src/interfaces/mining.h
index ff4f8710..7f69c758 100644
--- a/src/interfaces/mining.h
+++ b/src/interfaces/mining.h
@@ -180,6 +180,15 @@ public:
*/
virtual bool submitBlock(const CBlock& block, std::string& reason, std::string& debug) = 0;
+ /**
+ * Fetch raw transactions from the mempool by txid.
+ *
+ * @param[in] txids transaction ids to look up
+ * @returns one entry per requested txid containing the
+ * transaction if found, otherwise nullptr
+ */
+ virtual std::vector<CTransactionRef> getTransactionsByTxID(const std::vector<Txid>& txids) = 0;
+
//! Get internal node context. Useful for RPC and testing,
//! but not accessible across processes.
virtual const node::NodeContext* context() { return nullptr; }
diff --git a/src/ipc/capnp/mining.capnp b/src/ipc/capnp/mining.capnp
index a6dd8d71..e1c9e116 100644
--- a/src/ipc/capnp/mining.capnp
+++ b/src/ipc/capnp/mining.capnp
@@ -26,6 +26,7 @@ interface Mining $Proxy.wrap("interfaces::Mining") {
checkBlock @5 (context :Proxy.Context, block: Data, options: BlockCheckOptions) -> (reason: Text, debug: Text, result: Bool);
interrupt @6 () -> ();
submitBlock @7 (context :Proxy.Context, block: Data) -> (reason: Text, debug: Text, result: Bool);
+ getTransactionsByTxID @8 (context :Proxy.Context, txids: List(Data)) -> (result: List(Data));
}
interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") {
diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
index 641252d2..33473fbf 100644
--- a/src/node/interfaces.cpp
+++ b/src/node/interfaces.cpp
@@ -1035,6 +1035,19 @@ public:
return accepted && new_block && reason.empty();
}
+ std::vector<CTransactionRef> getTransactionsByTxID(const std::vector<Txid>& txids) override
+ {
+ if (!m_node.mempool) return {};
+
+ std::vector<CTransactionRef> results;
+ results.reserve(txids.size());
+ LOCK(m_node.mempool->cs);
+ for (const auto& txid : txids) {
+ results.emplace_back(m_node.mempool->get(txid));
+ }
+ return results;
+ }
+
const NodeContext* context() override { return &m_node; }
ChainstateManager& chainman() { return *Assert(m_node.chainman); }
KernelNotifications& notifications() { return *Assert(m_node.notifications); }
diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp
index fd9559b5..a366e5e4 100644
--- a/src/test/miner_tests.cpp
+++ b/src/test/miner_tests.cpp
@@ -184,6 +184,16 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
const auto high_fee_tx{entry.Fee(50000).Time(Now<NodeSeconds>()).SpendsCoinbase(false).FromTx(tx)};
TryAddToMempool(tx_mempool, high_fee_tx);
+ // Test getTransactionsByTxID()
+ const std::vector<Txid> tx_id_list{
+ hashParentTx,
+ Txid::FromUint256(uint256::ZERO) // non-existing tx
+ };
+ auto raw_txs = mining->getTransactionsByTxID(tx_id_list);
+ BOOST_REQUIRE_EQUAL(raw_txs.size(), tx_id_list.size());
+ BOOST_CHECK(raw_txs[0]);
+ BOOST_CHECK(raw_txs[0]->GetHash() == hashParentTx);
+ BOOST_CHECK(!raw_txs[1]);
block_template = mining->createNewBlock(options, /*cooldown=*/false);
BOOST_REQUIRE(block_template);
block = block_template->getBlock();
diff --git a/test/functional/interface_ipc_mining.py b/test/functional/interface_ipc_mining.py
index 4cbc6eb8..a81e76da 100755
--- a/test/functional/interface_ipc_mining.py
+++ b/test/functional/interface_ipc_mining.py
@@ -672,6 +672,30 @@ class IPCMiningTest(BitcoinTestFramework):
asyncio.run(capnp.run(async_routine()))
+ def run_transaction_lookup_test(self):
+ """Test getTransactionsByTxID()."""
+ self.log.info("Running transaction lookup test")
+
+ async def async_routine():
+ ctx, mining = await make_mining_ctx(self)
+ tx1 = self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
+ tx2 = self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0], utxo_to_spend=tx1["new_utxo"])
+
+ self.log.debug("getTransactionsByTxID() returns mempool txs and nulls")
+ raw_txs_txid = await mining.getTransactionsByTxID(ctx, [tx1["tx"].txid, tx2["tx"].txid, bytes(32)])
+ assert_equal(len(raw_txs_txid.result), 3)
+ assert_equal(raw_txs_txid.result[0].hex(), tx1["hex"])
+ assert_equal(raw_txs_txid.result[1].hex(), tx2["hex"])
+ assert_equal(raw_txs_txid.result[2], b'')
+
+ self.log.debug("Mined transactions are not returned")
+ self.generate(self.nodes[0], 1)
+ self.sync_all()
+ raw_txs = await mining.getTransactionsByTxID(ctx, [tx1["tx"].txid])
+ assert_equal(raw_txs.result[0], b'')
+
+ asyncio.run(capnp.run(async_routine()))
+
def run_low_height_test(self):
"""Test that IPC createNewBlock() works at low block heights on a
clean chain, in particular with regard to bad-cb-length.
@@ -727,6 +751,7 @@ class IPCMiningTest(BitcoinTestFramework):
self.run_waitnext_mining_policy_test()
self.run_block_max_weight_test()
self.run_ipc_option_override_test()
+ self.run_transaction_lookup_test()
# Needs to run last because it resets the chain.
self.run_low_height_test()
diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py
index a0f2a174..7614bd92 100755
--- a/test/functional/test_framework/messages.py
+++ b/test/functional/test_framework/messages.py
@@ -697,15 +697,20 @@ class CTransaction:
"""Return wtxid (transaction hash with witness) as integer."""
return uint256_from_str(hash256(self.serialize_with_witness()))
+ @property
+ def txid(self):
+ """Return txid (transaction hash without witness) as little-endian bytes."""
+ return hash256(self.serialize_without_witness())
+
@property
def txid_hex(self):
"""Return txid (transaction hash without witness) as hex string."""
- return hash256(self.serialize_without_witness())[::-1].hex()
+ return self.txid[::-1].hex()
@property
def txid_int(self):
"""Return txid (transaction hash without witness) as integer."""
- return uint256_from_str(hash256(self.serialize_without_witness()))
+ return uint256_from_str(self.txid)
def is_valid(self):
for tout in self.vout:
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.