@@ -134,6 +134,74 @@ impl ZebraDb {
134134 }
135135}
136136
137+ fn checkpoint_replay_bond_rewards (
138+ db : & ZebraDb ,
139+ bonds_modified_in_block : & HashMap < BondKey , DelegationBond > ,
140+ statuses_modified_in_block : & HashMap < BondKey , BondStatus > ,
141+ ) -> Vec < ( BondKey , u64 ) > {
142+ let mut active_bonds: Vec < ( BondKey , DelegationBond ) > = db
143+ . all_bonds ( )
144+ . filter_map ( |( bond_key, bond, status) | {
145+ let status = statuses_modified_in_block
146+ . get ( & bond_key)
147+ . copied ( )
148+ . unwrap_or ( status) ;
149+
150+ status. is_active ( ) . then_some ( (
151+ bond_key,
152+ bonds_modified_in_block
153+ . get ( & bond_key)
154+ . copied ( )
155+ . unwrap_or ( bond) ,
156+ ) )
157+ } )
158+ . collect ( ) ;
159+
160+ for ( bond_key, bond) in bonds_modified_in_block {
161+ if db. delegation_bond ( bond_key) . is_none ( )
162+ && statuses_modified_in_block
163+ . get ( bond_key)
164+ . is_some_and ( BondStatus :: is_active)
165+ {
166+ active_bonds. push ( ( * bond_key, * bond) ) ;
167+ }
168+ }
169+
170+ if active_bonds. is_empty ( ) {
171+ return Vec :: new ( ) ;
172+ }
173+
174+ let total_staked_zats: u64 = active_bonds
175+ . iter ( )
176+ . map ( |( _, bond) | u64:: from ( bond. amount ) )
177+ . sum ( ) ;
178+ let Some ( max_staker) = active_bonds
179+ . iter ( )
180+ . max_by_key ( |( bond_key, bond) | ( u64:: from ( bond. amount ) , std:: cmp:: Reverse ( * bond_key) ) )
181+ . map ( |( bond_key, _) | * bond_key)
182+ else {
183+ return Vec :: new ( ) ;
184+ } ;
185+
186+ let bond_reward_total = 500_000_000u64 ;
187+ let mut paid_reward = 0u64 ;
188+ let mut rewards = Vec :: with_capacity ( active_bonds. len ( ) ) ;
189+
190+ for ( bond_key, bond) in active_bonds {
191+ if bond_key == max_staker {
192+ continue ;
193+ }
194+
195+ let reward = ( ( u64:: from ( bond. amount ) as u128 ) * ( bond_reward_total as u128 )
196+ / ( total_staked_zats as u128 ) ) as u64 ;
197+ paid_reward += reward;
198+ rewards. push ( ( bond_key, reward) ) ;
199+ }
200+
201+ rewards. push ( ( max_staker, bond_reward_total - paid_reward) ) ;
202+ rewards
203+ }
204+
137205impl DiskWriteBatch {
138206 /// Prepare a database batch containing the delegation bonds in `finalized.block`,
139207 /// and return it (without actually writing anything).
@@ -150,9 +218,10 @@ impl DiskWriteBatch {
150218 // Process transactions to update bond state
151219 use zcash_primitives:: transaction:: StakingActionKind ;
152220
153- // Track bonds created in this block so we can apply rewards to them
154- // (they won't be in the DB yet when we apply rewards)
155- let mut bonds_created_in_block: HashMap < BondKey , DelegationBond > = HashMap :: new ( ) ;
221+ // Track bonds modified in this block so we can apply rewards to the post-block bond state
222+ // (they won't be in the DB yet when we apply rewards).
223+ let mut bonds_modified_in_block: HashMap < BondKey , DelegationBond > = HashMap :: new ( ) ;
224+ let mut statuses_modified_in_block: HashMap < BondKey , BondStatus > = HashMap :: new ( ) ;
156225
157226 // Iterate through all transactions in the block
158227 for ( transaction_index, transaction) in finalized. block . transactions . iter ( ) . enumerate ( ) {
@@ -172,18 +241,41 @@ impl DiskWriteBatch {
172241 DelegationBond :: new ( amount, target_finalizer, transaction_location) ;
173242
174243 // Track this bond for reward application
175- bonds_created_in_block. insert ( bond_key, bond. clone ( ) ) ;
244+ bonds_modified_in_block. insert ( bond_key, bond. clone ( ) ) ;
245+ statuses_modified_in_block. insert ( bond_key, BondStatus :: Active ) ;
176246
177247 // Insert new bond
178248 self . prepare_new_delegation_bond ( & db. db , bond_key, bond) ;
179249 }
180250 StakingActionKind :: BeginDelegationUnbonding => {
181251 // Mark bond as unbonding
182- self . prepare_unbonding_delegation_bond ( & db. db , db, bond_key, transaction_location) ?;
252+ self . prepare_unbonding_delegation_bond (
253+ & db. db ,
254+ db,
255+ bond_key,
256+ transaction_location,
257+ ) ?;
258+ statuses_modified_in_block. insert (
259+ bond_key,
260+ BondStatus :: Unbonding {
261+ unbonded_at : transaction_location,
262+ } ,
263+ ) ;
183264 }
184265 StakingActionKind :: WithdrawDelegationBond => {
185266 // Mark bond as withdrawn
186- self . prepare_withdrawn_delegation_bond ( & db. db , db, bond_key, transaction_location) ?;
267+ self . prepare_withdrawn_delegation_bond (
268+ & db. db ,
269+ db,
270+ bond_key,
271+ transaction_location,
272+ ) ?;
273+ statuses_modified_in_block. insert (
274+ bond_key,
275+ BondStatus :: Withdrawn {
276+ withdrawn_at : transaction_location,
277+ } ,
278+ ) ;
187279 }
188280 StakingActionKind :: RetargetDelegationBond => {
189281 // Update the bond's target_finalizer
@@ -193,7 +285,7 @@ impl DiskWriteBatch {
193285 db,
194286 bond_key,
195287 new_target,
196- & mut bonds_created_in_block ,
288+ & mut bonds_modified_in_block ,
197289 ) ?;
198290 }
199291 // Other staking actions don't affect delegation bonds
@@ -202,12 +294,29 @@ impl DiskWriteBatch {
202294 }
203295 }
204296
205- // Apply bond rewards accumulated in the non-finalized state
297+ // Apply bond rewards accumulated in the non-finalized state.
298+ // Checkpoint-style replay, used by copy-state and rollback-tip-height, bypasses the
299+ // non-finalized state. In that case, derive the same deterministic rewards from the
300+ // finalized bond table plus this block's pending bond/status changes.
301+ let derived_bond_rewards;
302+ let bond_rewards = if finalized. bond_rewards . is_empty ( ) {
303+ derived_bond_rewards = checkpoint_replay_bond_rewards (
304+ db,
305+ & bonds_modified_in_block,
306+ & statuses_modified_in_block,
307+ ) ;
308+ & derived_bond_rewards
309+ } else {
310+ & finalized. bond_rewards
311+ } ;
312+
206313 let delegation_bond_by_key_cf = db. db . cf_handle ( DELEGATION_BOND_BY_KEY ) . unwrap ( ) ;
207- for ( bond_key, reward_amount) in & finalized . bond_rewards {
314+ for ( bond_key, reward_amount) in bond_rewards {
208315 // Get current bond from bonds modified in this block first, then fall back to DB.
209316 // This ensures we use the updated bond if it was retargeted/created in this block.
210- let bond_opt = bonds_created_in_block. get ( bond_key) . cloned ( )
317+ let bond_opt = bonds_modified_in_block
318+ . get ( bond_key)
319+ . cloned ( )
211320 . or_else ( || db. delegation_bond ( bond_key) ) ;
212321
213322 if let Some ( mut bond) = bond_opt {
@@ -226,7 +335,12 @@ impl DiskWriteBatch {
226335 /// Inserts into:
227336 /// - `delegation_bond_by_key`: stores the bond data
228337 /// - `bond_status_by_key`: stores Active status
229- fn prepare_new_delegation_bond ( & mut self , db : & DiskDb , bond_key : BondKey , bond : DelegationBond ) {
338+ fn prepare_new_delegation_bond (
339+ & mut self ,
340+ db : & DiskDb ,
341+ bond_key : BondKey ,
342+ bond : DelegationBond ,
343+ ) {
230344 let delegation_bond_by_key_cf = db. cf_handle ( DELEGATION_BOND_BY_KEY ) . unwrap ( ) ;
231345 let bond_status_by_key_cf = db. cf_handle ( BOND_STATUS_BY_KEY ) . unwrap ( ) ;
232346
@@ -330,10 +444,13 @@ impl DiskWriteBatch {
330444
331445 // Get current bond from bonds modified in this block first, then fall back to DB.
332446 // This ensures we use the updated bond if it was modified earlier in this block.
333- let bond_opt = bonds_created_in_block. get ( & bond_key) . cloned ( )
447+ let bond_opt = bonds_created_in_block
448+ . get ( & bond_key)
449+ . cloned ( )
334450 . or_else ( || zebra_db. delegation_bond ( & bond_key) ) ;
335451
336- let mut bond = bond_opt. ok_or_else ( || format ! ( "bond {:?} not found for retarget" , bond_key) ) ?;
452+ let mut bond =
453+ bond_opt. ok_or_else ( || format ! ( "bond {:?} not found for retarget" , bond_key) ) ?;
337454
338455 // Update only target_finalizer (not created_at or amount)
339456 bond. target_finalizer = new_target;
0 commit comments