diff --git a/src/interfaces/mining.h b/src/interfaces/mining.h index ff4f87109f9..7f69c7580db 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 getTransactionsByTxID(const std::vector& 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 a6dd8d71f31..e1c9e116c26 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 641252d2afa..33473fbf4ed 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -1035,6 +1035,19 @@ public: return accepted && new_block && reason.empty(); } + std::vector getTransactionsByTxID(const std::vector& txids) override + { + if (!m_node.mempool) return {}; + + std::vector 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 fd9559b543c..a366e5e494d 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()).SpendsCoinbase(false).FromTx(tx)}; TryAddToMempool(tx_mempool, high_fee_tx); + // Test getTransactionsByTxID() + const std::vector 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 4cbc6eb8c54..a81e76da05b 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 a0f2a174023..7614bd92c0b 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: