Skip to content

Commit 35a1f92

Browse files
committed
State: Replay staking data during checkpoint copy
1 parent 5b69a22 commit 35a1f92

2 files changed

Lines changed: 214 additions & 23 deletions

File tree

zebra-crosslink/zebra-state/src/service/finalized_state/zebra_db/chain.rs

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//! each time the database format (column, serialization, etc) changes.
1313
1414
use std::{
15-
collections::{BTreeMap, HashMap},
15+
collections::{BTreeMap, HashMap, HashSet},
1616
sync::Arc,
1717
};
1818

@@ -268,25 +268,35 @@ impl DiskWriteBatch {
268268

269269
// Apply bond rewards to the staking_bonded pool tally FIRST,
270270
// before unbonding moves value between pools (to match non-finalized state order)
271-
let total_rewards: u64 = finalized.bond_rewards.iter().map(|(_, amount)| amount).sum();
271+
let total_rewards: u64 = if finalized.bond_rewards.is_empty() {
272+
checkpoint_replay_reward_total(db, finalized)
273+
} else {
274+
finalized
275+
.bond_rewards
276+
.iter()
277+
.map(|(_, amount)| amount)
278+
.sum()
279+
};
272280
if total_rewards > 0 {
273281
let current_bonded = new_value_pool.staking_bonded_amount();
274-
let new_bonded: Amount<NonNegative> = (current_bonded + Amount::try_from(total_rewards as i64)?)
275-
.expect("staking_bonded pool should not overflow from rewards");
282+
let new_bonded: Amount<NonNegative> = (current_bonded
283+
+ Amount::try_from(total_rewards as i64)?)
284+
.expect("staking_bonded pool should not overflow from rewards");
276285
new_value_pool.set_staking_bonded_amount(new_bonded);
277286
}
278287

279288
// Handle BeginDelegationUnbonding staking actions.
280289
// These move value from staking_bonded to staking_unbonded, but this transfer
281290
// is not captured by chain_value_pool_change (which only handles value entering/leaving pools).
282-
// Use the pre-computed unbonding_amounts which include rewards from the non-finalized state.
283-
for (_bond_key, bond_amount) in &finalized.unbonding_amounts {
284-
let bond_amount: Amount<NonNegative> = Amount::try_from(*bond_amount as i64)?;
285-
291+
// Use the pre-computed unbonding_amounts from the non-finalized state when available.
292+
// Checkpoint-style replay, used by copy-state and rollback-tip-height, bypasses the
293+
// non-finalized state and therefore has to recover the amount from the finalized bond table.
294+
let unbonding_amounts = finalized_unbonding_amounts(db, finalized)?;
295+
for bond_amount in unbonding_amounts {
286296
// Move value from staking_bonded to staking_unbonded
287297
let current_bonded = new_value_pool.staking_bonded_amount();
288-
let new_bonded: Amount<NonNegative> = (current_bonded - bond_amount)
289-
.expect("staking_bonded pool should not underflow");
298+
let new_bonded: Amount<NonNegative> =
299+
(current_bonded - bond_amount).expect("staking_bonded pool should not underflow");
290300
new_value_pool.set_staking_bonded_amount(new_bonded);
291301

292302
let current_unbonded = new_value_pool.staking_unbonded_amount();
@@ -314,3 +324,67 @@ impl DiskWriteBatch {
314324
Ok(())
315325
}
316326
}
327+
328+
fn checkpoint_replay_reward_total(db: &ZebraDb, finalized: &FinalizedBlock) -> u64 {
329+
let mut active_bonds: HashSet<_> = db
330+
.all_bonds()
331+
.filter_map(|(bond_key, _bond, status)| status.is_active().then_some(bond_key))
332+
.collect();
333+
334+
for transaction in &finalized.block.transactions {
335+
let Some(staking_action) = transaction.staking_action() else {
336+
continue;
337+
};
338+
339+
match staking_action.kind {
340+
StakingActionKind::CreateNewDelegationBond => {
341+
active_bonds.insert(staking_action.arg32_0);
342+
}
343+
StakingActionKind::BeginDelegationUnbonding
344+
| StakingActionKind::WithdrawDelegationBond => {
345+
active_bonds.remove(&staking_action.arg32_0);
346+
}
347+
_ => {}
348+
}
349+
}
350+
351+
if active_bonds.is_empty() {
352+
0
353+
} else {
354+
500_000_000
355+
}
356+
}
357+
358+
fn finalized_unbonding_amounts(
359+
db: &ZebraDb,
360+
finalized: &FinalizedBlock,
361+
) -> Result<Vec<Amount<NonNegative>>, BoxError> {
362+
if !finalized.unbonding_amounts.is_empty() {
363+
return finalized
364+
.unbonding_amounts
365+
.iter()
366+
.map(|(_bond_key, bond_amount)| {
367+
let bond_amount = i64::try_from(*bond_amount)?;
368+
Amount::try_from(bond_amount).map_err(Into::into)
369+
})
370+
.collect();
371+
}
372+
373+
finalized
374+
.block
375+
.transactions
376+
.iter()
377+
.filter_map(|transaction| {
378+
let staking_action = transaction.staking_action()?;
379+
(staking_action.kind == StakingActionKind::BeginDelegationUnbonding)
380+
.then_some(staking_action.arg32_0)
381+
})
382+
.map(|bond_key| {
383+
db.delegation_bond(&bond_key)
384+
.map(|bond| bond.amount)
385+
.ok_or_else(|| {
386+
format!("bond {:?} not found while updating value pools", bond_key).into()
387+
})
388+
})
389+
.collect()
390+
}

zebra-crosslink/zebra-state/src/service/finalized_state/zebra_db/delegation.rs

Lines changed: 130 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
137205
impl 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

Comments
 (0)