From 3c415c85c70623caf737914ab07b247b67ad97a5 Mon Sep 17 00:00:00 2001 From: Matias Fontanini Date: Mon, 2 Feb 2026 13:08:12 -0800 Subject: [PATCH 1/5] chore: don't share rewards state --- keeper/src/l2/mod.rs | 21 ------ keeper/src/l2/rewards.rs | 144 ++++++++++++++++----------------------- 2 files changed, 59 insertions(+), 106 deletions(-) diff --git a/keeper/src/l2/mod.rs b/keeper/src/l2/mod.rs index ce57427..45401a1 100644 --- a/keeper/src/l2/mod.rs +++ b/keeper/src/l2/mod.rs @@ -23,7 +23,6 @@ struct RoundState { outcome: Option, round_info: Option, rewards_done: bool, - reward_sync_attempted: bool, jailing_done: bool, } @@ -34,28 +33,8 @@ struct RoundInfoView { invalid_stake: U256, } -#[derive(Debug, Clone)] -struct RewardPolicyCache { - last_checked_at: Option, - last_budget: Option, - last_remaining: Option, - last_sync_attempt_at: Option, -} - -impl RewardPolicyCache { - fn new() -> Self { - Self { - last_checked_at: None, - last_budget: None, - last_remaining: None, - last_sync_attempt_at: None, - } - } -} - #[derive(Default)] pub struct KeeperState { raw_htx_by_heartbeat: HashMap, rounds: HashMap, - reward_policies: HashMap, } diff --git a/keeper/src/l2/rewards.rs b/keeper/src/l2/rewards.rs index 23c34fe..d18dfe3 100644 --- a/keeper/src/l2/rewards.rs +++ b/keeper/src/l2/rewards.rs @@ -1,10 +1,10 @@ use crate::{ clients::{L2KeeperClient, RewardPolicyInstance}, - l2::{KeeperState, RewardPolicyCache, RoundInfoView, RoundKey}, + l2::{KeeperState, RoundInfoView, RoundKey}, metrics, }; use alloy::primitives::{Address, U256, map::HashMap, utils::format_units}; -use anyhow::{Context, bail}; +use anyhow::{Context, anyhow, bail}; use blacklight_contract_clients::{ ProtocolConfig::ProtocolConfigInstance, common::{errors::decode_any_error, overestimate_gas}, @@ -24,10 +24,19 @@ struct TokenContext { address: Address, } +#[derive(Clone, Copy)] +struct RewardsContext { + token: TokenContext, + last_checked_at: Option, + last_budget: Option, + last_remaining: Option, + last_sync_attempt_at: Option, +} + pub(crate) struct RewardsDistributor { client: Arc, state: Arc>, - token_context: HashMap, + rewards_context: HashMap, } impl RewardsDistributor { @@ -35,7 +44,7 @@ impl RewardsDistributor { Self { client, state, - token_context: Default::default(), + rewards_context: Default::default(), } } @@ -54,7 +63,7 @@ impl RewardsDistributor { .call() .await .context("Failed to get reward policy contract address")?; - let token = self.fetch_token_context(reward_policy_address).await?; + let token = self.fetch_token_context(reward_policy_address).await?.token; let reward_policy = self.client.reward_policy(reward_policy_address); let erc20 = self.client.erc20(token.address); @@ -187,53 +196,39 @@ impl RewardsDistributor { return Ok(false); } - let mut cache = { - let state = self.state.lock().await; - state - .reward_policies - .get(&reward_address) - .cloned() - .unwrap_or_else(RewardPolicyCache::new) - }; - let reward_policy = self.client.reward_policy(reward_address); + let ctx = self.fetch_token_context(reward_address).await?; let mut budget = None; - if cache.last_checked_at == Some(block_timestamp) { - budget = cache.last_budget; + if ctx.last_checked_at == Some(block_timestamp) { + budget = ctx.last_budget; } if budget.is_none() { let fetched = reward_policy.spendableBudget().call().await?; budget = Some(fetched); - cache.last_checked_at = Some(block_timestamp); - cache.last_budget = Some(fetched); - cache.last_remaining = None; + ctx.last_checked_at = Some(block_timestamp); + ctx.last_budget = Some(fetched); + ctx.last_remaining = None; } let budget = budget.unwrap_or(U256::ZERO); metrics::get().l2.rewards.set_budget(budget); if budget > U256::ZERO { - self.store_reward_cache(reward_address, cache).await; return Ok(true); } - let remaining = if let Some(value) = cache.last_remaining { + let remaining = if let Some(value) = ctx.last_remaining { value } else { let value = reward_policy.streamRemaining().call().await?; - cache.last_remaining = Some(value); + ctx.last_remaining = Some(value); value }; - let token_ctx = self.fetch_token_context(reward_address).await?; - let should_unlock = if remaining > U256::ZERO { - self.can_unlock_budget( - &reward_policy, - remaining, - block_timestamp, - token_ctx.decimals, - ) - .await? - } else { - false - }; + let should_unlock = Self::can_unlock_budget( + &reward_policy, + remaining, + block_timestamp, + ctx.token.decimals, + ) + .await?; if !should_unlock { info!( @@ -242,42 +237,19 @@ impl RewardsDistributor { reward = ?reward_address, "Reward budget still unlocking, skipping" ); - self.store_reward_cache(reward_address, cache).await; return Ok(false); } - let already_attempted = { - let mut state = self.state.lock().await; - let entry = state.rounds.entry(key).or_default(); - if entry.reward_sync_attempted { - true - } else { - entry.reward_sync_attempted = true; - false - } - }; - if already_attempted { - debug!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, - reward = ?reward_address, - "Reward sync already attempted for round, skipping" - ); - self.store_reward_cache(reward_address, cache).await; - return Ok(false); - } - - if cache.last_sync_attempt_at == Some(block_timestamp) { + if ctx.last_sync_attempt_at == Some(block_timestamp) { debug!( heartbeat_key = ?key.heartbeat_key, round = key.round, reward = ?reward_address, "Reward sync already attempted for reward policy in this tick" ); - self.store_reward_cache(reward_address, cache).await; return Ok(false); } - cache.last_sync_attempt_at = Some(block_timestamp); + ctx.last_sync_attempt_at = Some(block_timestamp); info!( heartbeat_key = ?key.heartbeat_key, @@ -305,7 +277,6 @@ impl RewardsDistributor { error = %decode_any_error(&e), "Reward policy sync failed" ); - self.store_reward_cache(reward_address, cache).await; return Ok(false); } } @@ -324,18 +295,15 @@ impl RewardsDistributor { reward = ?reward_address, "{}", skip_msg ); - cache.last_budget = Some(budget_after); - self.store_reward_cache(reward_address, cache).await; + ctx.last_budget = Some(budget_after); return Ok(false); } - cache.last_budget = Some(budget_after); - self.store_reward_cache(reward_address, cache).await; + ctx.last_budget = Some(budget_after); Ok(true) } async fn can_unlock_budget( - &self, reward_policy: &RewardPolicyInstance, remaining: U256, block_timestamp: u64, @@ -370,11 +338,6 @@ impl RewardsDistributor { Ok(unlocked >= threshold) } - async fn store_reward_cache(&self, reward_address: Address, cache: RewardPolicyCache) { - let mut state = self.state.lock().await; - state.reward_policies.insert(reward_address, cache); - } - async fn build_voter_list( &self, key: RoundKey, @@ -403,22 +366,33 @@ impl RewardsDistributor { Ok((voters, total_weight)) } - async fn fetch_token_context(&mut self, address: Address) -> anyhow::Result { - if let Some(context) = self.token_context.get(&address) { - return Ok(*context); + async fn fetch_token_context( + &mut self, + address: Address, + ) -> anyhow::Result<&mut RewardsContext> { + if !self.rewards_context.contains_key(&address) { + info!("Fetching token context for rewards policy address {address}"); + let reward_policy = RewardPolicyInstance::new(address, self.client.provider()); + let token_address = reward_policy.rewardToken().call().await?; + let erc20 = self.client.erc20(token_address); + let decimals = erc20.decimals().call().await?; + + let token = TokenContext { + decimals, + address: token_address, + }; + let context = RewardsContext { + token, + last_checked_at: None, + last_budget: None, + last_remaining: None, + last_sync_attempt_at: None, + }; + self.rewards_context.insert(address, context); } - info!("Fetching token context for rewards policy address {address}"); - let reward_policy = RewardPolicyInstance::new(address, self.client.provider()); - let token_address = reward_policy.rewardToken().call().await?; - let erc20 = self.client.erc20(token_address); - let decimals = erc20.decimals().call().await?; - - let context = TokenContext { - decimals, - address: token_address, - }; - self.token_context.insert(address, context); - Ok(context) + self.rewards_context + .get_mut(&address) + .ok_or_else(|| anyhow!("insertion gone")) } } From fa304700e8313ebd8f6017fd9f8e0dd1c917a069 Mon Sep 17 00:00:00 2001 From: Matias Fontanini Date: Mon, 2 Feb 2026 13:18:29 -0800 Subject: [PATCH 2/5] chore: remove duplicate logging parameters --- keeper/src/l2/rewards.rs | 70 ++++++++-------------------------------- 1 file changed, 14 insertions(+), 56 deletions(-) diff --git a/keeper/src/l2/rewards.rs b/keeper/src/l2/rewards.rs index d18dfe3..7fcc63c 100644 --- a/keeper/src/l2/rewards.rs +++ b/keeper/src/l2/rewards.rs @@ -11,7 +11,7 @@ use blacklight_contract_clients::{ }; use std::sync::Arc; use tokio::sync::Mutex; -use tracing::{debug, info, warn}; +use tracing::{debug, info, instrument, warn}; const MIN_NIL_SYNC_THRESHOLD: u64 = 100; const RESPONDED_BIT: u64 = 1 << 2; @@ -87,6 +87,7 @@ impl RewardsDistributor { Ok(()) } + #[instrument(skip_all, fields(key = ?key.heartbeat_key, round = key.round))] pub(crate) async fn distribute_rewards( &mut self, block_timestamp: u64, @@ -120,7 +121,7 @@ impl RewardsDistributor { } }; if !self - .ensure_reward_budget(block_timestamp, round_info.reward, key) + .ensure_reward_budget(block_timestamp, round_info.reward) .await? { return Ok(()); @@ -138,8 +139,6 @@ impl RewardsDistributor { if sum_weights != expected_stake { warn!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, sum_weights = ?sum_weights, expected_stake = ?expected_stake, "Reward weights mismatch, skipping" @@ -147,12 +146,7 @@ impl RewardsDistributor { return Ok(()); } - info!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, - voters = voters.len(), - "Distributing rewards" - ); + info!(voters = voters.len(), "Distributing rewards"); let call = self.client @@ -163,8 +157,6 @@ impl RewardsDistributor { Ok(pending) => { let receipt = pending.get_receipt().await?; info!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, tx_hash = ?receipt.transaction_hash, "Rewards distributed" ); @@ -181,18 +173,14 @@ impl RewardsDistributor { } } + #[instrument(skip_all, fields(reward = ?reward_address))] async fn ensure_reward_budget( &mut self, block_timestamp: u64, reward_address: Address, - key: RoundKey, ) -> anyhow::Result { if reward_address == Address::ZERO { - warn!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, - "Reward policy address is zero, skipping" - ); + warn!("Reward policy address is zero, skipping"); return Ok(false); } @@ -231,52 +219,28 @@ impl RewardsDistributor { .await?; if !should_unlock { - info!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, - reward = ?reward_address, - "Reward budget still unlocking, skipping" - ); + info!("Reward budget still unlocking, skipping"); return Ok(false); } if ctx.last_sync_attempt_at == Some(block_timestamp) { - debug!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, - reward = ?reward_address, - "Reward sync already attempted for reward policy in this tick" - ); + debug!("Reward sync already attempted for reward policy in this tick"); return Ok(false); } ctx.last_sync_attempt_at = Some(block_timestamp); - info!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, - reward = ?reward_address, - "Reward budget unlocking, syncing policy", - ); + info!("Reward budget unlocking, syncing policy",); match reward_policy.sync().send().await { Ok(pending) => { let receipt = pending.get_receipt().await?; info!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, - reward = ?reward_address, tx_hash = ?receipt.transaction_hash, "Reward policy synced" ); } Err(e) => { - warn!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, - reward = ?reward_address, - error = %decode_any_error(&e), - "Reward policy sync failed" - ); + warn!("Reward policy sync failed: {}", decode_any_error(&e)); return Ok(false); } } @@ -284,17 +248,11 @@ impl RewardsDistributor { let budget_after = reward_policy.spendableBudget().call().await?; if budget_after == U256::ZERO { let remaining_after = reward_policy.streamRemaining().call().await?; - let skip_msg = if remaining_after > U256::ZERO { - "Reward budget still unlocking after sync, skipping" + if remaining_after > U256::ZERO { + info!("Reward budget still unlocking after sync, skipping"); } else { - "Reward budget still empty after sync, skipping" - }; - info!( - heartbeat_key = ?key.heartbeat_key, - round = key.round, - reward = ?reward_address, - "{}", skip_msg - ); + info!("Reward budget still empty after sync, skipping"); + } ctx.last_budget = Some(budget_after); return Ok(false); } From 03cd338eea49ddbd1e9f1d1403ab590e64621b1e Mon Sep 17 00:00:00 2001 From: Matias Fontanini Date: Mon, 2 Feb 2026 14:47:32 -0800 Subject: [PATCH 3/5] chore: simplify contract state fetching --- keeper/src/l2/rewards.rs | 76 ++++++++++++++++++---------------------- keeper/src/metrics.rs | 24 +++++++++---- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/keeper/src/l2/rewards.rs b/keeper/src/l2/rewards.rs index 7fcc63c..60a58a4 100644 --- a/keeper/src/l2/rewards.rs +++ b/keeper/src/l2/rewards.rs @@ -27,10 +27,10 @@ struct TokenContext { #[derive(Clone, Copy)] struct RewardsContext { token: TokenContext, - last_checked_at: Option, - last_budget: Option, - last_remaining: Option, - last_sync_attempt_at: Option, + checked_at: u64, + synced_at: u64, + spendable: U256, + remaining: U256, } pub(crate) struct RewardsDistributor { @@ -186,33 +186,29 @@ impl RewardsDistributor { let reward_policy = self.client.reward_policy(reward_address); let ctx = self.fetch_token_context(reward_address).await?; - let mut budget = None; - if ctx.last_checked_at == Some(block_timestamp) { - budget = ctx.last_budget; - } - if budget.is_none() { - let fetched = reward_policy.spendableBudget().call().await?; - budget = Some(fetched); - ctx.last_checked_at = Some(block_timestamp); - ctx.last_budget = Some(fetched); - ctx.last_remaining = None; + if ctx.checked_at != block_timestamp { + // Fetch the current spendable/remaining budgets + ctx.spendable = reward_policy.spendableBudget().call().await?; + ctx.remaining = reward_policy.streamRemaining().call().await?; + ctx.checked_at = block_timestamp; + metrics::get().l2.rewards.set_spendable(ctx.spendable); + metrics::get().l2.rewards.set_remaining(ctx.remaining); + + let spendable = format_units(ctx.spendable, ctx.token.decimals)?; + let remaining = format_units(ctx.remaining, ctx.token.decimals)?; + info!( + spendable = spendable, + remaining = remaining, + "Fetched contract context" + ); } - let budget = budget.unwrap_or(U256::ZERO); - metrics::get().l2.rewards.set_budget(budget); - if budget > U256::ZERO { + if ctx.spendable > U256::ZERO { return Ok(true); } - let remaining = if let Some(value) = ctx.last_remaining { - value - } else { - let value = reward_policy.streamRemaining().call().await?; - ctx.last_remaining = Some(value); - value - }; let should_unlock = Self::can_unlock_budget( &reward_policy, - remaining, + ctx.remaining, block_timestamp, ctx.token.decimals, ) @@ -223,14 +219,13 @@ impl RewardsDistributor { return Ok(false); } - if ctx.last_sync_attempt_at == Some(block_timestamp) { + if ctx.synced_at == block_timestamp { debug!("Reward sync already attempted for reward policy in this tick"); return Ok(false); } - ctx.last_sync_attempt_at = Some(block_timestamp); + ctx.synced_at = block_timestamp; info!("Reward budget unlocking, syncing policy",); - match reward_policy.sync().send().await { Ok(pending) => { let receipt = pending.get_receipt().await?; @@ -245,20 +240,19 @@ impl RewardsDistributor { } } - let budget_after = reward_policy.spendableBudget().call().await?; - if budget_after == U256::ZERO { - let remaining_after = reward_policy.streamRemaining().call().await?; - if remaining_after > U256::ZERO { + // Update our state after syncing + ctx.spendable = reward_policy.spendableBudget().call().await?; + ctx.remaining = reward_policy.streamRemaining().call().await?; + if ctx.spendable == U256::ZERO { + if ctx.remaining > U256::ZERO { info!("Reward budget still unlocking after sync, skipping"); } else { info!("Reward budget still empty after sync, skipping"); } - ctx.last_budget = Some(budget_after); - return Ok(false); + Ok(false) + } else { + Ok(true) } - - ctx.last_budget = Some(budget_after); - Ok(true) } async fn can_unlock_budget( @@ -341,10 +335,10 @@ impl RewardsDistributor { }; let context = RewardsContext { token, - last_checked_at: None, - last_budget: None, - last_remaining: None, - last_sync_attempt_at: None, + checked_at: 0, + synced_at: 0, + spendable: U256::ZERO, + remaining: U256::ZERO, }; self.rewards_context.insert(address, context); } diff --git a/keeper/src/metrics.rs b/keeper/src/metrics.rs index 8b09ae9..4bc0d79 100644 --- a/keeper/src/metrics.rs +++ b/keeper/src/metrics.rs @@ -156,7 +156,8 @@ impl L2EventMetrics { pub(crate) struct L2RewardsMetrics { distribution: Counter, - budget: Gauge, + spendable: Gauge, + remaining: Gauge, } impl L2RewardsMetrics { @@ -165,13 +166,18 @@ impl L2RewardsMetrics { .u64_counter("blacklight.keeper.l2.rewards.distributions") .with_description("Number of times rewards were distributed") .build(); - let budget = meter - .f64_gauge("blacklight.keeper.l2.rewards.budget") - .with_description("The current spendable budget for rewards") + let spendable = meter + .f64_gauge("blacklight.keeper.l2.rewards.spendadble") + .with_description("The spendable budget for rewards") + .build(); + let remaining = meter + .f64_gauge("blacklight.keeper.l2.rewards.remaining") + .with_description("The remaining budget to be unlocked for rewards") .build(); Self { distribution, - budget, + spendable, + remaining, } } @@ -179,8 +185,12 @@ impl L2RewardsMetrics { self.distribution.add(1, &[]); } - pub(crate) fn set_budget(&self, value: U256) { - self.budget.record(value.into(), &[]); + pub(crate) fn set_spendable(&self, value: U256) { + self.spendable.record(value.into(), &[]); + } + + pub(crate) fn set_remaining(&self, value: U256) { + self.remaining.record(value.into(), &[]); } } From 0a8cdc351a53a29ad3d2cdf4dd9490f8a6af6d1c Mon Sep 17 00:00:00 2001 From: Matias Fontanini Date: Mon, 2 Feb 2026 14:50:16 -0800 Subject: [PATCH 4/5] chore: expose metric on stream end --- keeper/src/l2/rewards.rs | 1 + keeper/src/metrics.rs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/keeper/src/l2/rewards.rs b/keeper/src/l2/rewards.rs index 60a58a4..342342f 100644 --- a/keeper/src/l2/rewards.rs +++ b/keeper/src/l2/rewards.rs @@ -268,6 +268,7 @@ impl RewardsDistributor { let stream_rate = reward_policy.streamRatePerSecondWad().call().await?; let last_update = reward_policy.lastUpdate().call().await?; let stream_end = reward_policy.streamEnd().call().await?; + metrics::get().l2.rewards.set_end(stream_end); if block_timestamp >= stream_end { return Ok(true); diff --git a/keeper/src/metrics.rs b/keeper/src/metrics.rs index 4bc0d79..97106cd 100644 --- a/keeper/src/metrics.rs +++ b/keeper/src/metrics.rs @@ -158,6 +158,7 @@ pub(crate) struct L2RewardsMetrics { distribution: Counter, spendable: Gauge, remaining: Gauge, + end: Gauge, } impl L2RewardsMetrics { @@ -174,10 +175,15 @@ impl L2RewardsMetrics { .f64_gauge("blacklight.keeper.l2.rewards.remaining") .with_description("The remaining budget to be unlocked for rewards") .build(); + let end = meter + .u64_gauge("blacklight.keeper.l2.rewards.end") + .with_description("The UNIX epoch timestamp when the current budget runs out") + .build(); Self { distribution, spendable, remaining, + end, } } @@ -192,6 +198,10 @@ impl L2RewardsMetrics { pub(crate) fn set_remaining(&self, value: U256) { self.remaining.record(value.into(), &[]); } + + pub(crate) fn set_end(&self, value: u64) { + self.end.record(value, &[]); + } } pub(crate) struct L2EscalationsMetrics { From 7739cef2b4143e5577b0e6962edadc014477ce89 Mon Sep 17 00:00:00 2001 From: Matias Fontanini Date: Mon, 2 Feb 2026 15:07:21 -0800 Subject: [PATCH 5/5] chore: don't keep round info in cache --- keeper/src/l2/mod.rs | 10 +--------- keeper/src/l2/rewards.rs | 39 ++++++++++----------------------------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/keeper/src/l2/mod.rs b/keeper/src/l2/mod.rs index 45401a1..6e81ce7 100644 --- a/keeper/src/l2/mod.rs +++ b/keeper/src/l2/mod.rs @@ -1,4 +1,4 @@ -use alloy::primitives::{Address, B256, Bytes, U256}; +use alloy::primitives::{Address, B256, Bytes}; use std::collections::HashMap; mod escalator; @@ -21,18 +21,10 @@ struct RoundState { raw_htx: Option, deadline: Option, outcome: Option, - round_info: Option, rewards_done: bool, jailing_done: bool, } -#[derive(Debug, Clone, Copy)] -struct RoundInfoView { - reward: Address, - valid_stake: U256, - invalid_stake: U256, -} - #[derive(Default)] pub struct KeeperState { raw_htx_by_heartbeat: HashMap, diff --git a/keeper/src/l2/rewards.rs b/keeper/src/l2/rewards.rs index 342342f..5ebf77e 100644 --- a/keeper/src/l2/rewards.rs +++ b/keeper/src/l2/rewards.rs @@ -1,6 +1,6 @@ use crate::{ clients::{L2KeeperClient, RewardPolicyInstance}, - l2::{KeeperState, RoundInfoView, RoundKey}, + l2::{KeeperState, RoundKey}, metrics, }; use alloy::primitives::{Address, U256, map::HashMap, utils::format_units}; @@ -95,33 +95,14 @@ impl RewardsDistributor { outcome: u8, members: Vec
, ) -> anyhow::Result<()> { - let cached_info = { - let state = self.state.lock().await; - state.rounds.get(&key).and_then(|round| round.round_info) - }; - let round_info = match cached_info { - Some(info) => info, - None => { - let info = self - .client - .heartbeat_manager() - .rounds(key.heartbeat_key, key.round) - .call() - .await?; - let view = RoundInfoView { - reward: info.reward, - valid_stake: info.validStake, - invalid_stake: info.invalidStake, - }; - let mut state = self.state.lock().await; - if let Some(round_state) = state.rounds.get_mut(&key) { - round_state.round_info = Some(view); - } - view - } - }; + let info = self + .client + .heartbeat_manager() + .rounds(key.heartbeat_key, key.round) + .call() + .await?; if !self - .ensure_reward_budget(block_timestamp, round_info.reward) + .ensure_reward_budget(block_timestamp, info.reward) .await? { return Ok(()); @@ -132,9 +113,9 @@ impl RewardsDistributor { .build_voter_list(key, &members, expected_verdict) .await?; let expected_stake = if outcome == 1 { - round_info.valid_stake + info.validStake } else { - round_info.invalid_stake + info.invalidStake }; if sum_weights != expected_stake {