rpc: [mempool] Remove erroneous Univalue integral casts
What changed, and why it matters
This commit fixes a bug in Bitcoin Core's mempool RPC output where fee values were incorrectly narrowed to 32-bit integers before being displayed. For very large fee bumps (over about 86 bitcoins), this cast could corrupt the reported 'chunkfee' and 'fees.chunk' values, making them look much smaller or even negative. The fix removes the unnecessary casts and adds a test for large fee deltas. It is a correctness bug in information shown to users and miners, not a direct theft-of-funds vulnerability.
Apply the patch. It is a low-risk correctness fix. Users and miners relying on mempool fee data for large transactions should upgrade or verify reported values. No emergency response is warranted.
Security signals we found
Integer narrowing/truncation of monetary values in RPC output
Incorrect fee reporting for large prioritised transactions
Potential for negative-looking fee values after sign truncation
No input validation bypass or memory corruption
Evidence from the diff
In src/rpc/mempool.cpp, two calls to ValueFromAmount() were wrapping chunk_feerate.fee and feerate.fee with an explicit (int) cast. Because these fee amounts are stored as CAmount (int64_t satoshis), casting to int truncates the value to a 32-bit signed integer. On platforms where int is 32 bits, any fee amount exceeding INT_MAX satoshis (~21.47 BTC) is misrepresented. The patch removes the casts so the full int64_t value is passed through. A functional test is added that uses an 86 BTC fee delta and asserts that getrawmempool and getmempoolcluster report the correct modified/chunk fees.
Changed components
src/rpc/mempool.cppgetrawmempool RPC (verbose fee fields)getmempoolcluster RPC (chunkfee field)prioritisetransaction large fee-delta handlingInspect captured patch +54 / −2
diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp
index a9a67834..7db47adf 100644
--- a/src/rpc/mempool.cpp
+++ b/src/rpc/mempool.cpp
@@ -315,7 +315,7 @@ static std::vector<RPCResult> MempoolEntryDescription()
void AppendChunkInfo(UniValue& all_chunks, FeePerWeight chunk_feerate, std::vector<const CTxMemPoolEntry *> chunk_txs)
{
UniValue chunk(UniValue::VOBJ);
- chunk.pushKV("chunkfee", ValueFromAmount((int)chunk_feerate.fee));
+ chunk.pushKV("chunkfee", ValueFromAmount(chunk_feerate.fee));
chunk.pushKV("chunkweight", chunk_feerate.size);
UniValue chunk_txids(UniValue::VARR);
for (const auto& chunk_tx : chunk_txs) {
@@ -383,7 +383,7 @@ static void entryToJSON(const CTxMemPool& pool, UniValue& info, const CTxMemPool
fees.pushKV("modified", ValueFromAmount(e.GetModifiedFee()));
fees.pushKV("ancestor", ValueFromAmount(ancestor_fees));
fees.pushKV("descendant", ValueFromAmount(descendant_fees));
- fees.pushKV("chunk", ValueFromAmount((int)feerate.fee));
+ fees.pushKV("chunk", ValueFromAmount(feerate.fee));
info.pushKV("fees", std::move(fees));
const CTransaction& tx = e.GetTx();
diff --git a/test/functional/mining_prioritisetransaction.py b/test/functional/mining_prioritisetransaction.py
index c3b56e8d..323c987c 100755
--- a/test/functional/mining_prioritisetransaction.py
+++ b/test/functional/mining_prioritisetransaction.py
@@ -36,6 +36,57 @@ class PrioritiseTransactionTest(BitcoinTestFramework):
node.prioritisetransaction(txid, 0, -delta)
assert_equal(node.getprioritisedtransactions(), {})
+ def test_large_fee_bump(self):
+ self.log.info("Test that a large fee delta is honoured")
+ tx = self.wallet.create_self_transfer()
+ txid = tx["txid"]
+ fee_delta = int(86 * COIN) # large enough to not fit into (u)int32_t
+ self.nodes[0].prioritisetransaction(txid=txid, fee_delta=fee_delta)
+ assert_equal(
+ self.nodes[0].getprioritisedtransactions(),
+ {
+ txid: {
+ "fee_delta": fee_delta,
+ "in_mempool": False,
+ },
+ },
+ )
+ self.nodes[0].sendrawtransaction(tx["hex"])
+ expected_modified_fee = tx["fee"] + Decimal(fee_delta) / COIN
+ assert_equal(
+ self.nodes[0].getprioritisedtransactions(),
+ {
+ txid: {
+ "fee_delta": fee_delta,
+ "in_mempool": True,
+ "modified_fee": int(expected_modified_fee * COIN),
+ },
+ },
+ )
+ # This transaction forms its own chunk.
+ mempool_entry = self.nodes[0].getrawmempool(verbose=True)[txid]
+ assert_equal(mempool_entry["fees"]["base"], tx["fee"])
+ assert_equal(mempool_entry["fees"]["modified"], expected_modified_fee)
+ assert_equal(mempool_entry["fees"]["ancestor"], expected_modified_fee)
+ assert_equal(mempool_entry["fees"]["descendant"], expected_modified_fee)
+ assert_equal(mempool_entry["fees"]["chunk"], expected_modified_fee)
+ assert_equal(mempool_entry["chunkweight"], mempool_entry["weight"])
+ append_chunk_info = self.nodes[0].getmempoolcluster(txid)
+ assert_equal(
+ append_chunk_info,
+ {
+ "clusterweight": mempool_entry["weight"],
+ "txcount": 1,
+ "chunks": [{
+ "chunkfee": expected_modified_fee,
+ "chunkweight": mempool_entry["weight"],
+ "txs": [txid],
+ }],
+ },
+ )
+ self.generate(self.nodes[0], 1)
+ assert_equal(self.nodes[0].getprioritisedtransactions(), {})
+
def test_replacement(self):
self.log.info("Test tx prioritisation stays after a tx is replaced")
conflicting_input = self.wallet.get_utxo()
@@ -175,6 +226,7 @@ class PrioritiseTransactionTest(BitcoinTestFramework):
# Test `prioritisetransaction` invalid `fee_delta`
assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].prioritisetransaction, txid=txid, fee_delta='foo')
+ self.test_large_fee_bump()
self.test_replacement()
self.test_diamond()
Why this scored 37/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.