Hi Zebra team - I think PR #10847 only fixes the shielded-coinbase re-proving cost for the empty-mempool case. With any fee-paying mempool transaction selected into the template, the new CoinbaseCache appears to churn between two keys inside every getblocktemplate call, so a short-polling miner still re-proves both the fake sizing coinbase and the final coinbase on each request.
This is not a consensus or security issue. It is a miner-facing performance regression / incomplete cache fix.
What I am seeing
The cache added in #10847 is a single slot:
Option<(block::Height, Amount<NonNegative>, TransactionTemplate<amount::NegativeOrZero>)>
It is keyed by (height, fee), but a normal non-empty block-template build uses two different fee keys:
select_mempool_transactions() asks for/builds a fake coinbase at Amount::zero() so ZIP-317 transaction selection can account for coinbase size and sigops.
BlockTemplateResponse::new_internal() then computes txs_fee from the selected transactions and asks for/builds the real coinbase at that nonzero fee.
Because the cache has only one slot, the zero-fee fake coinbase and the real-fee coinbase evict each other:
- call N ends with cache key
(height, real_fee);
- call N+1 ZIP-317 sizing looks up
(height, 0), misses, builds/proves the fake coinbase, and stores (height, 0);
- final template assembly looks up
(height, real_fee), misses, builds/proves the real coinbase, and stores (height, real_fee);
- repeat on the next short-poll request.
So the cache only reliably helps when txs_fee == 0, because then the fake and real coinbase share the same key.
Why this matters
For a transparent mining address this is mostly hidden. For a shielded miner address, both coinbase builds can involve expensive Sapling/Orchard/Ironwood proving. Under a fee-paying mempool, a short-polling miner can still pay the same repeated proof cost that #10847 intended to remove.
This is especially relevant for mining pools and for the NU6.3/Ironwood testnet path, where shielded coinbase behavior is part of the readiness surface.
Relevant code
Suggested reproduction
- Configure Zebra mining to a shielded address.
- Put at least one fee-paying transaction in the mempool so selected template fees are nonzero.
- Short-poll
getblocktemplate.
- Instrument
TransactionTemplate::new_coinbase() or cache hit/miss logging.
Expected: after the first request for a given (height, selected_fees), subsequent short-poll requests should not rebuild either coinbase unless the template inputs change.
Actual source-level behavior: each request alternates the single cache slot between (height, 0) and (height, selected_fees), causing repeated misses for both the fake and real coinbase.
Suggested fix
Make the cache hold at least two entries per active height, so the ZIP-317 fake coinbase and the final real-fee coinbase can coexist.
The smallest patch is probably a tiny two-entry cache keyed by (height, fee):
- on
get(), return an entry whose (height, fee) matches;
- on
store(), replace an existing matching key if present;
- otherwise insert into an empty slot;
- if both slots are full, evict a stale-height entry first, then fall back to evicting either entry.
A small HashMap<(Height, Amount<NonNegative>), TransactionTemplate<...>> bounded to the current height would also work, but two slots should cover the current fake-vs-real template path without widening memory behavior.
The existing randomize_coinbase_data() cache clear still needs to clear all entries, because miner params changed.
Test that would catch it
Add a unit test that uses one CoinbaseCache, stores/requests both:
(height, Amount::zero()) for the ZIP-317 fake coinbase, and
(height, nonzero_fee) for the final template coinbase.
The test should assert that both keys still hit after both have been stored. The current single-slot implementation cannot satisfy that.
Thanks for the #10847 work - this looks like a narrow cache-shape issue rather than a problem with the underlying template logic.
Hi Zebra team - I think PR #10847 only fixes the shielded-coinbase re-proving cost for the empty-mempool case. With any fee-paying mempool transaction selected into the template, the new
CoinbaseCacheappears to churn between two keys inside everygetblocktemplatecall, so a short-polling miner still re-proves both the fake sizing coinbase and the final coinbase on each request.This is not a consensus or security issue. It is a miner-facing performance regression / incomplete cache fix.
What I am seeing
The cache added in #10847 is a single slot:
It is keyed by
(height, fee), but a normal non-empty block-template build uses two different fee keys:select_mempool_transactions()asks for/builds a fake coinbase atAmount::zero()so ZIP-317 transaction selection can account for coinbase size and sigops.BlockTemplateResponse::new_internal()then computestxs_feefrom the selected transactions and asks for/builds the real coinbase at that nonzero fee.Because the cache has only one slot, the zero-fee fake coinbase and the real-fee coinbase evict each other:
(height, real_fee);(height, 0), misses, builds/proves the fake coinbase, and stores(height, 0);(height, real_fee), misses, builds/proves the real coinbase, and stores(height, real_fee);So the cache only reliably helps when
txs_fee == 0, because then the fake and real coinbase share the same key.Why this matters
For a transparent mining address this is mostly hidden. For a shielded miner address, both coinbase builds can involve expensive Sapling/Orchard/Ironwood proving. Under a fee-paying mempool, a short-polling miner can still pay the same repeated proof cost that #10847 intended to remove.
This is especially relevant for mining pools and for the NU6.3/Ironwood testnet path, where shielded coinbase behavior is part of the readiness surface.
Relevant code
zebra-rpc/src/methods/types/get_block_template.rsCoinbaseCacheis a singleOption<(height, fee, coinbase)>.BlockTemplateResponse::new_internal()looks up/stores by(height, txs_fee).zebra-rpc/src/methods/types/get_block_template/zip317.rsselect_mempool_transactions()looks up/stores the fake coinbase by(height, Amount::zero()).zebra-rpc/src/methods.rsget_block_template()callsselect_mempool_transactions(... Some(&coinbase_cache)), then callsBlockTemplateResponse::new_internal(... Some(self.gbt.coinbase_cache()), ...)for the same template.Suggested reproduction
getblocktemplate.TransactionTemplate::new_coinbase()or cache hit/miss logging.Expected: after the first request for a given
(height, selected_fees), subsequent short-poll requests should not rebuild either coinbase unless the template inputs change.Actual source-level behavior: each request alternates the single cache slot between
(height, 0)and(height, selected_fees), causing repeated misses for both the fake and real coinbase.Suggested fix
Make the cache hold at least two entries per active height, so the ZIP-317 fake coinbase and the final real-fee coinbase can coexist.
The smallest patch is probably a tiny two-entry cache keyed by
(height, fee):get(), return an entry whose(height, fee)matches;store(), replace an existing matching key if present;A small
HashMap<(Height, Amount<NonNegative>), TransactionTemplate<...>>bounded to the current height would also work, but two slots should cover the current fake-vs-real template path without widening memory behavior.The existing
randomize_coinbase_data()cache clear still needs to clear all entries, because miner params changed.Test that would catch it
Add a unit test that uses one
CoinbaseCache, stores/requests both:(height, Amount::zero())for the ZIP-317 fake coinbase, and(height, nonzero_fee)for the final template coinbase.The test should assert that both keys still hit after both have been stored. The current single-slot implementation cannot satisfy that.
Thanks for the #10847 work - this looks like a narrow cache-shape issue rather than a problem with the underlying template logic.