From e0463b4e8c25f8a5fe10999f2821e7b221d2e40a Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 9 Feb 2026 17:19:04 +0100 Subject: [PATCH] rpc: add coinbase_tx field to getblock This adds a "coinbase_tx" field to the getblock RPC result, starting at verbosity level 1. It contains only fields guaranteed to be small, i.e. not the outputs. --- doc/release-notes-34512.md | 8 ++++++++ src/rpc/blockchain.cpp | 30 ++++++++++++++++++++++++++++++ test/functional/rpc_blockchain.py | 23 +++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 doc/release-notes-34512.md diff --git a/doc/release-notes-34512.md b/doc/release-notes-34512.md new file mode 100644 index 00000000000..b863448853f --- /dev/null +++ b/doc/release-notes-34512.md @@ -0,0 +1,8 @@ +Updated RPCs +------------ + +- The `getblock` RPC now returns a `coinbase_tx` object at verbosity levels 1, 2, + and 3. It contains `version`, `locktime`, `sequence`, `coinbase` and + `witness`. This allows for efficiently querying coinbase + transaction properties without fetching the full transaction data at + verbosity 2+. (#34512) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index a883570f94b..ca343a4c251 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -181,6 +181,24 @@ UniValue blockheaderToJSON(const CBlockIndex& tip, const CBlockIndex& blockindex return result; } +/** Serialize coinbase transaction metadata */ +UniValue coinbaseTxToJSON(const CTransaction& coinbase_tx) +{ + CHECK_NONFATAL(!coinbase_tx.vin.empty()); + const CTxIn& vin_0{coinbase_tx.vin[0]}; + UniValue coinbase_tx_obj(UniValue::VOBJ); + coinbase_tx_obj.pushKV("version", coinbase_tx.version); + coinbase_tx_obj.pushKV("locktime", coinbase_tx.nLockTime); + coinbase_tx_obj.pushKV("sequence", vin_0.nSequence); + coinbase_tx_obj.pushKV("coinbase", HexStr(vin_0.scriptSig)); + const auto& witness_stack{vin_0.scriptWitness.stack}; + if (!witness_stack.empty()) { + CHECK_NONFATAL(witness_stack.size() == 1); + coinbase_tx_obj.pushKV("witness", HexStr(witness_stack[0])); + } + return coinbase_tx_obj; +} + UniValue blockToJSON(BlockManager& blockman, const CBlock& block, const CBlockIndex& tip, const CBlockIndex& blockindex, TxVerbosity verbosity, const uint256 pow_limit) { UniValue result = blockheaderToJSON(tip, blockindex, pow_limit); @@ -188,6 +206,10 @@ UniValue blockToJSON(BlockManager& blockman, const CBlock& block, const CBlockIn result.pushKV("strippedsize", ::GetSerializeSize(TX_NO_WITNESS(block))); result.pushKV("size", ::GetSerializeSize(TX_WITH_WITNESS(block))); result.pushKV("weight", ::GetBlockWeight(block)); + + CHECK_NONFATAL(!block.vtx.empty()); + result.pushKV("coinbase_tx", coinbaseTxToJSON(*block.vtx[0])); + UniValue txs(UniValue::VARR); txs.reserve(block.vtx.size()); @@ -760,6 +782,14 @@ static RPCHelpMan getblock() {RPCResult::Type::NUM, "size", "The block size"}, {RPCResult::Type::NUM, "strippedsize", "The block size excluding witness data"}, {RPCResult::Type::NUM, "weight", "The block weight as defined in BIP 141"}, + {RPCResult::Type::OBJ, "coinbase_tx", "Coinbase transaction metadata", + { + {RPCResult::Type::NUM, "version", "The coinbase transaction version"}, + {RPCResult::Type::NUM, "locktime", "The coinbase transaction's locktime (nLockTime)"}, + {RPCResult::Type::NUM, "sequence", "The coinbase input's sequence number (nSequence)"}, + {RPCResult::Type::STR_HEX, "coinbase", "The coinbase input's script"}, + {RPCResult::Type::STR_HEX, "witness", /*optional=*/true, "The coinbase input's first (and only) witness stack element, if present"}, + }}, {RPCResult::Type::NUM, "height", "The block height or index"}, {RPCResult::Type::NUM, "version", "The block version"}, {RPCResult::Type::STR_HEX, "versionHex", "The block version formatted in hexadecimal"}, diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 5c1f2ee4008..cecd0c03fac 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -646,6 +646,26 @@ class BlockchainTest(BitcoinTestFramework): self.wallet.send_self_transfer(fee_rate=fee_per_kb, from_node=node) blockhash = self.generate(node, 1)[0] + def assert_coinbase_metadata(hash, verbosity): + block = node.getblock(hash, verbosity) + coinbase_tx = node.getblock(hash, 2)["tx"][0] + + expected_keys = {"version", "locktime", "sequence", "coinbase"} + if "txinwitness" in coinbase_tx["vin"][0]: + expected_keys.add("witness") + assert_equal(set(block["coinbase_tx"].keys()), expected_keys) + + assert_equal(block["coinbase_tx"]["version"], coinbase_tx["version"]) + assert_equal(block["coinbase_tx"]["locktime"], coinbase_tx["locktime"]) + assert_equal(block["coinbase_tx"]["sequence"], coinbase_tx["vin"][0]["sequence"]) + assert_equal(block["coinbase_tx"]["coinbase"], coinbase_tx["vin"][0]["coinbase"]) + + witness_stack = coinbase_tx["vin"][0].get("txinwitness") + if witness_stack is None: + assert "witness" not in block["coinbase_tx"] + else: + assert_equal(block["coinbase_tx"]["witness"], witness_stack[0]) + def assert_hexblock_hashes(verbosity): block = node.getblock(blockhash, verbosity) assert_equal(blockhash, hash256(bytes.fromhex(block[:160]))[::-1].hex()) @@ -692,6 +712,9 @@ class BlockchainTest(BitcoinTestFramework): assert_fee_not_in_block(blockhash, 1) assert_fee_not_in_block(blockhash, True) + self.log.info("Test getblock coinbase metadata fields") + assert_coinbase_metadata(blockhash, 1) + self.log.info('Test that getblock with verbosity 2 and 3 includes expected fee') assert_fee_in_block(blockhash, 2) assert_fee_in_block(blockhash, 3)