Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions packages/rinku-node/src/checkpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1927,15 +1927,7 @@ impl CheckpointService {
.process_batch_special_txs_with_skip(&all_special_txs, &fast_path_already_finalized)
.await;

{
let state = self.state.inner.read().await;
let mut rewards = self.state.rewards.write().await;
for (addr, account) in &state.accounts {
if account.staked > 0 {
rewards.sync_stake_amount(addr, account.staked);
}
}
}
self.state.reconcile_rewards_stakes_from_accounts().await;

self.state
.process_batch_reward_infos(&contract_lane_txs, &batch_result.executed_hashes)
Expand Down
32 changes: 9 additions & 23 deletions packages/rinku-node/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,29 +454,13 @@ async fn main() -> Result<()> {
let genesis_addresses: std::collections::HashSet<String> =
genesis_seed.iter().map(|(a, _)| a.clone()).collect();

// Register genesis validators in RewardsService if missing.
// Do NOT blanket-remove non-genesis stakes — that confiscated user /
// delegator stakes on every genesis restart. Ghost redeploy validators
// are handled by reconcile_stakes_after_genesis_replace below.
{
use crate::validator_identity::GENESIS_VALIDATOR_STAKE;
let mut rewards = state.rewards.write().await;
if config.is_genesis_node {
let existing_stakes: Vec<String> = rewards
.get_all_stakes()
.iter()
.map(|s| s.staker.clone())
.collect();
let mut removed = 0;
for staker in &existing_stakes {
if !genesis_addresses.contains(staker) {
rewards.remove_stake(staker);
removed += 1;
}
}
if removed > 0 {
info!(
"Removed {} stale stake(s) from rewards service (not in genesis set)",
removed
);
}
}
let mut registered = 0;
for (address, _) in &genesis_seed {
if rewards.get_stake(address).is_none() {
Expand Down Expand Up @@ -572,11 +556,13 @@ async fn main() -> Result<()> {
}
}
}
// Clean up ghost accounts on ALL nodes (not just genesis)
state.cleanup_stale_accounts(&genesis_addresses).await;
// Preserve user stakes; only clear redeploy-ghost validators (+ empty accounts).
state
.reconcile_stakes_after_genesis_replace(&genesis_addresses)
.await;
} else {
let empty_set = std::collections::HashSet::new();
state.cleanup_stale_accounts(&empty_set).await;
state.cleanup_empty_ghost_accounts(&empty_set).await;
}

// Sync stakes to accounts AFTER genesis validator replacement
Expand Down
76 changes: 74 additions & 2 deletions packages/rinku-node/src/rewards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,22 @@ impl RewardsService {
}
}

/// Align `rewards.stakes` with the canonical `account.staked` value.
///
/// `canonical_amount == 0` removes the rewards entry entirely so ghost
/// stakes cannot linger after account state has already been cleared
/// (previously we only synced when account.staked > 0, leaving orphans).
pub fn sync_stake_amount(&mut self, staker: &str, canonical_amount: u64) {
if canonical_amount == 0 {
if self.stakes.remove(staker).is_some() {
tracing::info!(
"STAKE SYNC REMOVE: {} rewards.stakes cleared (canonical account.staked=0)",
&staker[..16.min(staker.len())]
);
}
return;
}

if let Some(existing) = self.stakes.get_mut(staker) {
if existing.amount != canonical_amount {
tracing::info!(
Expand All @@ -426,7 +441,7 @@ impl RewardsService {
);
existing.amount = canonical_amount;
}
} else if canonical_amount > 0 {
} else {
tracing::info!(
"STAKE SYNC CREATE: {} creating rewards.stakes entry with amount {} (canonical account.staked, no prior entry)",
&staker[..16.min(staker.len())],
Expand Down Expand Up @@ -846,7 +861,7 @@ impl RewardsService {
}
}

fn add_pending_reward(&mut self, address: &str, amount: u64) {
pub(crate) fn add_pending_reward(&mut self, address: &str, amount: u64) {
let pending = self.pending_rewards.entry(address.to_string()).or_insert(0);
*pending += amount;
}
Expand Down Expand Up @@ -1150,6 +1165,63 @@ mod tests {
assert!(unstake_result.is_err());
}

#[test]
fn sync_stake_amount_zero_removes_entry() {
let mut service = RewardsService::new(RewardConfig::default());
service.stake("user", to_micro_units(199.0), "tx1").unwrap();
assert!(service.get_stake("user").is_some());
assert_eq!(service.get_total_staked(), to_micro_units(199.0));

service.sync_stake_amount("user", 0);
assert!(
service.get_stake("user").is_none(),
"canonical account.staked=0 must remove rewards.stakes entry"
);
assert_eq!(service.get_total_staked(), 0);
assert_eq!(service.get_active_validators().len(), 0);
}

#[test]
fn sync_stake_amount_zero_is_noop_when_absent() {
let mut service = RewardsService::new(RewardConfig::default());
service.sync_stake_amount("nobody", 0);
assert_eq!(service.get_total_staked(), 0);
}

#[test]
fn sync_stake_amount_updates_and_creates() {
let mut service = RewardsService::new(RewardConfig::default());
service.stake("user", to_micro_units(100.0), "tx1").unwrap();
service.sync_stake_amount("user", to_micro_units(250.0));
assert_eq!(
service.get_stake("user").unwrap().amount,
to_micro_units(250.0)
);

service.sync_stake_amount("other", to_micro_units(100.0));
assert_eq!(
service.get_stake("other").unwrap().amount,
to_micro_units(100.0)
);
assert_eq!(service.get_total_staked(), to_micro_units(350.0));
}

#[test]
fn pending_rewards_survive_stake_removal() {
let mut service = RewardsService::new(RewardConfig::default());
service.stake("user", to_micro_units(200.0), "tx1").unwrap();
service.add_pending_reward("user", to_micro_units(11.0));
assert!(service.get_pending_rewards("user") > 0);

service.sync_stake_amount("user", 0);
assert!(service.get_stake("user").is_none());
assert_eq!(
service.get_pending_rewards("user"),
to_micro_units(11.0),
"pending rewards must not be wiped when stake entry is removed"
);
}

#[test]
fn test_tip_rewards() {
let mut service = RewardsService::new(RewardConfig::default());
Expand Down
Loading
Loading