mining: add getTransactionsByWitnessID() IPC method
What changed, and why it matters
This commit adds a new way for external mining software connected to Bitcoin Core over IPC to look up transactions in the memory pool by their witness transaction ID (wtxid). It mirrors an existing transaction lookup by regular transaction ID (txid) and includes tests. There is no indication in the commit that this fixes a security bug; it appears to be a normal feature addition.
No security action required; review as part of normal code review for the IPC/mining interface feature.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces getTransactionsByWitnessID() to the Mining IPC interface, its Cap’n Proto proxy definition, the node-side implementation that locks the mempool and returns CTransactionRefs, and unit/functional tests. It also refactors CTransaction.wtxid handling in the Python test framework to expose a raw wtxid property. The implementation is symmetric to the existing getTransactionsByTxID() and uses the same mempool lookup pattern.
Changed components
src/interfaces/mining.hsrc/ipc/capnp/mining.capnpsrc/node/interfaces.cpptest/functional/interface_ipc_mining.pytest/functional/test_framework/messages.pysrc/test/miner_tests.cppInspect captured patch +51 / −3
diff --git a/src/interfaces/mining.h b/src/interfaces/mining.h
index 7f69c758..884a1e16 100644
--- a/src/interfaces/mining.h
+++ b/src/interfaces/mining.h
@@ -189,6 +189,15 @@ public:
*/
virtual std::vector<CTransactionRef> getTransactionsByTxID(const std::vector<Txid>& txids) = 0;
+ /**
+ * Fetch raw transactions from the mempool by wtxid.
+ *
+ * @param[in] wtxids witness transaction ids to look up
+ * @returns one entry per requested wtxid containing the
+ * transaction if found, otherwise nullptr
+ */
+ virtual std::vector<CTransactionRef> getTransactionsByWitnessID(const std::vector<Wtxid>& wtxids) = 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 e1c9e116..5f0347fc 100644
--- a/src/ipc/capnp/mining.capnp
+++ b/src/ipc/capnp/mining.capnp
@@ -27,6 +27,7 @@ interface Mining $Proxy.wrap("interfaces::Mining") {
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));
+ getTransactionsByWitnessID @9 (context :Proxy.Context, wtxids: List(Data)) -> (result: List(Data));
}
interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") {
diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
index 33473fbf..dafca125 100644
--- a/src/node/interfaces.cpp
+++ b/src/node/interfaces.cpp
@@ -1048,6 +1048,19 @@ public:
return results;
}
+ std::vector<CTransactionRef> getTransactionsByWitnessID(const std::vector<Wtxid>& wtxids) override
+ {
+ if (!m_node.mempool) return {};
+
+ std::vector<CTransactionRef> results;
+ results.reserve(wtxids.size());
+ LOCK(m_node.mempool->cs);
+ for (const auto& wtxid : wtxids) {
+ results.emplace_back(m_node.mempool->get(wtxid));
+ }
+ 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 a366e5e4..17644dea 100644
--- a/src/test/miner_tests.cpp
+++ b/src/test/miner_tests.cpp
@@ -194,6 +194,17 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
BOOST_CHECK(raw_txs[0]);
BOOST_CHECK(raw_txs[0]->GetHash() == hashParentTx);
BOOST_CHECK(!raw_txs[1]);
+ // Test getTransactionsByWitnessID()
+ // tx has no witness, so just cast to Wtxid
+ const std::vector<Wtxid> wtx_id_list{
+ Wtxid::FromUint256(hashParentTx.ToUint256()),
+ Wtxid::FromUint256(uint256::ZERO)
+ };
+ raw_txs = mining->getTransactionsByWitnessID(wtx_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 a81e76da..f023b12d 100755
--- a/test/functional/interface_ipc_mining.py
+++ b/test/functional/interface_ipc_mining.py
@@ -673,7 +673,7 @@ class IPCMiningTest(BitcoinTestFramework):
asyncio.run(capnp.run(async_routine()))
def run_transaction_lookup_test(self):
- """Test getTransactionsByTxID()."""
+ """Test getTransactionsByTxID() and getTransactionsByWitnessID()."""
self.log.info("Running transaction lookup test")
async def async_routine():
@@ -688,11 +688,20 @@ class IPCMiningTest(BitcoinTestFramework):
assert_equal(raw_txs_txid.result[1].hex(), tx2["hex"])
assert_equal(raw_txs_txid.result[2], b'')
+ self.log.debug("getTransactionsByWitnessID() returns mempool txs and nulls")
+ raw_txs_wtxid = await mining.getTransactionsByWitnessID(ctx, [tx1["tx"].wtxid, tx2["tx"].wtxid, bytes(32)])
+ assert_equal(len(raw_txs_wtxid.result), 3)
+ assert_equal(raw_txs_wtxid.result[0].hex(), tx1["hex"])
+ assert_equal(raw_txs_wtxid.result[1].hex(), tx2["hex"])
+ assert_equal(raw_txs_wtxid.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'')
+ raw_txs = await mining.getTransactionsByWitnessID(ctx, [tx1["tx"].wtxid])
+ assert_equal(raw_txs.result[0], b'')
asyncio.run(capnp.run(async_routine()))
diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py
index 7614bd92..9c5b15ec 100755
--- a/test/functional/test_framework/messages.py
+++ b/test/functional/test_framework/messages.py
@@ -687,15 +687,20 @@ class CTransaction:
def serialize(self):
return self.serialize_with_witness()
+ @property
+ def wtxid(self):
+ """Return wtxid (transaction hash with witness) as little-endian bytes."""
+ return hash256(self.serialize_with_witness())
+
@property
def wtxid_hex(self):
"""Return wtxid (transaction hash with witness) as hex string."""
- return hash256(self.serialize())[::-1].hex()
+ return self.wtxid[::-1].hex()
@property
def wtxid_int(self):
"""Return wtxid (transaction hash with witness) as integer."""
- return uint256_from_str(hash256(self.serialize_with_witness()))
+ return uint256_from_str(self.wtxid)
@property
def txid(self):
Why this scored 18/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.