rpc: fix getblockstats UTXO overhead accounting
What changed, and why it matters
This commit fixes an accounting error in a Bitcoin Core RPC command called getblockstats. The command reports how much UTXO set size grows when a block is processed. The old code incorrectly counted the coinbase flag as a separate boolean byte, but in reality it is packed into the same 32-bit value as the block height. The fix removes that extra byte from the overhead calculation and updates the corresponding test expectations. It is a correctness bug in statistics output, not a security vulnerability that lets anyone steal funds or crash nodes.
No security response required. Treat as a normal bugfix/correctness patch. Users relying on getblockstats for UTXO growth metrics should expect slightly lower utxo_size_inc values after upgrading.
Security signals we found
Incorrect size accounting in non-consensus RPC statistics
No memory safety, authentication, or network parsing changes
No consensus, wallet, or mempool logic modified
Evidence from the diff
The getblockstats RPC computes utxo_size_inc and utxo_size_inc_actual by adding a PER_UTXO_OVERHEAD constant to the serialized size of each coin. The constant previously included sizeof(COutPoint) + sizeof(uint32_t) + sizeof(bool), treating nHeight and fCoinBase as separate fields. However, Coin packs nHeight and fCoinBase into a single 32-bit value (nHeightAndIsCoinbase), so the sizeof(bool) overcounts by one byte per UTXO created. The patch removes the bool term and adjusts the functional test JSON/Python expectations accordingly. This only affects the returned statistics values, not consensus, mempool, wallet, or network behavior.
Changed components
src/rpc/blockchain.cpptest/functional/data/rpc_getblockstats.jsontest/functional/rpc_getblockstats.pyInspect captured patch +11 / −11
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index 9838bda6..eba10f0d 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -1898,8 +1898,8 @@ static inline bool SetHasKeys(const std::set<T>& set, const Tk& key, const Args&
return (set.contains(key)) || SetHasKeys(set, args...);
}
-// outpoint (needed for the utxo index) + nHeight + fCoinBase
-static constexpr size_t PER_UTXO_OVERHEAD = sizeof(COutPoint) + sizeof(uint32_t) + sizeof(bool);
+// outpoint (needed for the utxo index) + nHeight|fCoinBase
+static constexpr size_t PER_UTXO_OVERHEAD = sizeof(COutPoint) + sizeof(uint32_t);
static RPCHelpMan getblockstats()
{
diff --git a/test/functional/data/rpc_getblockstats.json b/test/functional/data/rpc_getblockstats.json
index 7d7460aa..e0c0eb39 100644
--- a/test/functional/data/rpc_getblockstats.json
+++ b/test/functional/data/rpc_getblockstats.json
@@ -143,8 +143,8 @@
"txs": 1,
"utxo_increase": 2,
"utxo_increase_actual": 1,
- "utxo_size_inc": 163,
- "utxo_size_inc_actual": 75
+ "utxo_size_inc": 161,
+ "utxo_size_inc_actual": 74
},
{
"avgfee": 4440,
@@ -182,8 +182,8 @@
"txs": 2,
"utxo_increase": 3,
"utxo_increase_actual": 2,
- "utxo_size_inc": 235,
- "utxo_size_inc_actual": 147
+ "utxo_size_inc": 232,
+ "utxo_size_inc_actual": 145
},
{
"avgfee": 21390,
@@ -221,8 +221,8 @@
"txs": 5,
"utxo_increase": 6,
"utxo_increase_actual": 4,
- "utxo_size_inc": 441,
- "utxo_size_inc_actual": 300
+ "utxo_size_inc": 435,
+ "utxo_size_inc_actual": 296
}
]
}
\ No newline at end of file
diff --git a/test/functional/rpc_getblockstats.py b/test/functional/rpc_getblockstats.py
index 29d70a05..75e09639 100755
--- a/test/functional/rpc_getblockstats.py
+++ b/test/functional/rpc_getblockstats.py
@@ -171,16 +171,16 @@ class GetblockstatsTest(BitcoinTestFramework):
genesis_stats = self.nodes[0].getblockstats(0)
assert_equal(genesis_stats["blockhash"], "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206")
assert_equal(genesis_stats["utxo_increase"], 1)
- assert_equal(genesis_stats["utxo_size_inc"], 117)
+ assert_equal(genesis_stats["utxo_size_inc"], 116)
assert_equal(genesis_stats["utxo_increase_actual"], 0)
assert_equal(genesis_stats["utxo_size_inc_actual"], 0)
self.log.info('Test tip including OP_RETURN')
tip_stats = self.nodes[0].getblockstats(tip)
assert_equal(tip_stats["utxo_increase"], 6)
- assert_equal(tip_stats["utxo_size_inc"], 441)
+ assert_equal(tip_stats["utxo_size_inc"], 435)
assert_equal(tip_stats["utxo_increase_actual"], 4)
- assert_equal(tip_stats["utxo_size_inc_actual"], 300)
+ assert_equal(tip_stats["utxo_size_inc_actual"], 296)
self.log.info("Test when only header is known")
block = self.generateblock(self.nodes[0], output="raw(55)", transactions=[], submit=False)
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.