Skip to main content

espresso_types/v0/impls/
stake_table.rs

1use std::{
2    cmp::min,
3    collections::{HashMap, HashSet},
4    str::FromStr,
5    time::{Duration, Instant},
6};
7#[cfg(feature = "node")]
8use std::{future::Future, sync::Arc};
9
10#[cfg(feature = "node")]
11use alloy::{
12    eips::{BlockId, BlockNumberOrTag},
13    primitives::utils::format_ether,
14    providers::Provider,
15    rpc::types::Filter,
16    sol_types::{SolEvent, SolEventInterface},
17};
18use alloy::{
19    primitives::{Address, U256},
20    rpc::types::Log,
21};
22#[cfg(feature = "node")]
23use anyhow::bail;
24use anyhow::{Context, ensure};
25use ark_ec::AffineRepr;
26use ark_serialize::CanonicalSerialize;
27use ark_std::One;
28#[cfg(any(feature = "node", test, feature = "testing"))]
29use async_lock::Mutex as AsyncMutex;
30#[cfg(feature = "node")]
31use async_lock::RwLock as AsyncRwLock;
32use bigdecimal::BigDecimal;
33use committable::{Commitment, Committable, RawCommitmentBuilder};
34use futures::future::BoxFuture;
35#[cfg(feature = "node")]
36use hotshot_contract_adapter::sol_types::{
37    EspToken::{self, EspTokenInstance},
38    StakeTableV3,
39};
40use hotshot_contract_adapter::{
41    sol_types::StakeTableV3::{
42        CommissionUpdated, ConsensusKeysUpdated, ConsensusKeysUpdatedV2, Delegated, P2pAddrUpdated,
43        StakeTableV3Events, Undelegated, UndelegatedV2, ValidatorExit, ValidatorExitV2,
44        ValidatorRegistered, ValidatorRegisteredV2, ValidatorRegisteredV3, X25519KeyUpdated,
45    },
46    stake_table::StakeTableSolError,
47};
48use hotshot_types::{
49    addr::NetAddr,
50    data::{EpochNumber, vid_disperse::VID_TARGET_TOTAL_STAKE},
51    signature_key::{BLSPubKey, SchnorrPubKey},
52    traits::signature_key::SignatureKey as _,
53    x25519,
54};
55use humantime::format_duration;
56use indexmap::IndexMap;
57use itertools::Itertools;
58use num_traits::{FromPrimitive, Zero};
59use thiserror::Error;
60#[cfg(feature = "node")]
61use tokio::spawn;
62use tokio::time::sleep;
63#[cfg(feature = "node")]
64use tracing::Instrument;
65use vbs::version::Version;
66
67#[cfg(feature = "node")]
68use super::L1Client;
69#[cfg(any(test, feature = "testing"))]
70use super::v0_3::DAMembers;
71use super::{
72    Header,
73    traits::StateCatchup,
74    v0_3::{
75        AuthenticatedValidator, ChainConfig, EventKey, Fetcher, MAX_VALIDATORS,
76        RegisteredValidator, StakeTableEvent,
77    },
78};
79#[cfg(feature = "node")]
80use super::{traits::MembershipPersistence, v0_3::StakeTableUpdateTask};
81#[cfg(feature = "node")]
82use crate::traits::EventsPersistenceRead;
83#[cfg(feature = "node")]
84use crate::v0_1::L1Provider;
85#[cfg(feature = "node")]
86use crate::v0_3::{BLOCKS_PER_YEAR, INFLATION_RATE};
87use crate::v0_3::{
88    COMMISSION_BASIS_POINTS, EventSortingError, ExpectedStakeTableError, FetchRewardError,
89    MILLISECONDS_PER_YEAR, RewardAmount, StakeTableError,
90};
91
92pub type RegisteredValidatorMap = IndexMap<Address, RegisteredValidator<BLSPubKey>>;
93pub type AuthenticatedValidatorMap = IndexMap<Address, AuthenticatedValidator<BLSPubKey>>;
94
95pub fn to_registered_validator_map(
96    validators: &AuthenticatedValidatorMap,
97) -> RegisteredValidatorMap {
98    validators
99        .iter()
100        .map(|(addr, v)| (*addr, v.clone().into()))
101        .collect()
102}
103
104pub type StakeTableHash = Commitment<StakeTableState>;
105
106/// The result of applying a stake table event:
107/// - `Ok(Ok(()))`: success
108/// - `Ok(Err(...))`: expected error
109/// - `Err(...)`: serious error
110type ApplyEventResult<T> = Result<Result<T, ExpectedStakeTableError>, StakeTableError>;
111
112/// Format the alloy Log RPC type in a way to make it easy to find the event in an explorer.
113#[cfg_attr(not(feature = "node"), allow(dead_code))]
114trait DisplayLog {
115    fn display(&self) -> String;
116}
117
118impl DisplayLog for Log {
119    fn display(&self) -> String {
120        // These values are all unlikely to be missing because we only create Log variables by
121        // fetching them from the RPC, so for simplicity we use defaults if the any of the values
122        // are missing.
123        let block = self.block_number.unwrap_or_default();
124        let index = self.log_index.unwrap_or_default();
125        let hash = self.transaction_hash.unwrap_or_default();
126        format!("Log(block={block},index={index},transaction_hash={hash})")
127    }
128}
129
130impl TryFrom<StakeTableV3Events> for StakeTableEvent {
131    type Error = anyhow::Error;
132
133    fn try_from(value: StakeTableV3Events) -> anyhow::Result<Self> {
134        match value {
135            StakeTableV3Events::ValidatorRegistered(v) => Ok(StakeTableEvent::Register(v)),
136            StakeTableV3Events::ValidatorRegisteredV2(v) => Ok(StakeTableEvent::RegisterV2(v)),
137            StakeTableV3Events::ValidatorRegisteredV3(v) => Ok(StakeTableEvent::RegisterV3(v)),
138            StakeTableV3Events::ValidatorExit(v) => Ok(StakeTableEvent::Deregister(v)),
139            StakeTableV3Events::ValidatorExitV2(v) => Ok(StakeTableEvent::DeregisterV2(v)),
140            StakeTableV3Events::Delegated(v) => Ok(StakeTableEvent::Delegate(v)),
141            StakeTableV3Events::Undelegated(v) => Ok(StakeTableEvent::Undelegate(v)),
142            StakeTableV3Events::UndelegatedV2(v) => Ok(StakeTableEvent::UndelegateV2(v)),
143            StakeTableV3Events::ConsensusKeysUpdated(v) => Ok(StakeTableEvent::KeyUpdate(v)),
144            StakeTableV3Events::ConsensusKeysUpdatedV2(v) => Ok(StakeTableEvent::KeyUpdateV2(v)),
145            StakeTableV3Events::CommissionUpdated(v) => Ok(StakeTableEvent::CommissionUpdate(v)),
146            StakeTableV3Events::X25519KeyUpdated(v) => Ok(StakeTableEvent::X25519KeyUpdate(v)),
147            StakeTableV3Events::P2pAddrUpdated(v) => Ok(StakeTableEvent::P2pAddrUpdate(v)),
148            StakeTableV3Events::ExitEscrowPeriodUpdated(v) => Err(anyhow::anyhow!(
149                "Unsupported StakeTableV3Events::ExitEscrowPeriodUpdated({v:?})"
150            )),
151            StakeTableV3Events::Initialized(v) => Err(anyhow::anyhow!(
152                "Unsupported StakeTableV3Events::Initialized({v:?})"
153            )),
154            StakeTableV3Events::MaxCommissionIncreaseUpdated(v) => Err(anyhow::anyhow!(
155                "Unsupported StakeTableV3Events::MaxCommissionIncreaseUpdated({v:?})"
156            )),
157            StakeTableV3Events::MinCommissionUpdateIntervalUpdated(v) => Err(anyhow::anyhow!(
158                "Unsupported StakeTableV3Events::MinCommissionUpdateIntervalUpdated({v:?})"
159            )),
160            StakeTableV3Events::OwnershipTransferred(v) => Err(anyhow::anyhow!(
161                "Unsupported StakeTableV3Events::OwnershipTransferred({v:?})"
162            )),
163            StakeTableV3Events::Paused(v) => Err(anyhow::anyhow!(
164                "Unsupported StakeTableV3Events::Paused({v:?})"
165            )),
166            StakeTableV3Events::RoleAdminChanged(v) => Err(anyhow::anyhow!(
167                "Unsupported StakeTableV3Events::RoleAdminChanged({v:?})"
168            )),
169            StakeTableV3Events::RoleGranted(v) => Err(anyhow::anyhow!(
170                "Unsupported StakeTableV3Events::RoleGranted({v:?})"
171            )),
172            StakeTableV3Events::RoleRevoked(v) => Err(anyhow::anyhow!(
173                "Unsupported StakeTableV3Events::RoleRevoked({v:?})"
174            )),
175            StakeTableV3Events::Unpaused(v) => Err(anyhow::anyhow!(
176                "Unsupported StakeTableV3Events::Unpaused({v:?})"
177            )),
178            StakeTableV3Events::Upgraded(v) => Err(anyhow::anyhow!(
179                "Unsupported StakeTableV3Events::Upgraded({v:?})"
180            )),
181            StakeTableV3Events::WithdrawalClaimed(v) => Err(anyhow::anyhow!(
182                "Unsupported StakeTableV3Events::WithdrawalClaimed({v:?})"
183            )),
184            StakeTableV3Events::ValidatorExitClaimed(v) => Err(anyhow::anyhow!(
185                "Unsupported StakeTableV3Events::ValidatorExitClaimed({v:?})"
186            )),
187            StakeTableV3Events::Withdrawal(v) => Err(anyhow::anyhow!(
188                "Unsupported StakeTableV3Events::Withdrawal({v:?})"
189            )),
190            StakeTableV3Events::MetadataUriUpdated(v) => Err(anyhow::anyhow!(
191                "Unsupported StakeTableV3Events::MetadataUriUpdated({v:?})"
192            )),
193            StakeTableV3Events::MinDelegateAmountUpdated(v) => Err(anyhow::anyhow!(
194                "Unsupported StakeTableV3Events::MinDelegateAmountUpdated({v:?})"
195            )),
196        }
197    }
198}
199
200#[cfg_attr(not(feature = "node"), allow(dead_code))]
201fn sort_stake_table_events(
202    event_logs: Vec<(StakeTableV3Events, Log)>,
203) -> Result<Vec<(EventKey, StakeTableEvent)>, EventSortingError> {
204    let mut events: Vec<(EventKey, StakeTableEvent)> = Vec::new();
205
206    let key = |log: &Log| -> Result<EventKey, EventSortingError> {
207        let block_number = log
208            .block_number
209            .ok_or(EventSortingError::MissingBlockNumber)?;
210        let log_index = log.log_index.ok_or(EventSortingError::MissingLogIndex)?;
211        Ok((block_number, log_index))
212    };
213
214    for (e, log) in event_logs {
215        let k = key(&log)?;
216        let evt: StakeTableEvent = e
217            .try_into()
218            .map_err(|_| EventSortingError::InvalidStakeTableEvent)?;
219        events.push((k, evt));
220    }
221
222    events.sort_by_key(|(key, _)| *key);
223    Ok(events)
224}
225
226#[derive(Clone, Debug, Default, PartialEq)]
227pub struct StakeTableState {
228    validators: RegisteredValidatorMap,
229    validator_exits: HashSet<Address>,
230    used_bls_keys: HashSet<BLSPubKey>,
231    used_schnorr_keys: HashSet<SchnorrPubKey>,
232    used_x25519_keys: HashSet<x25519::PublicKey>,
233}
234
235impl Committable for StakeTableState {
236    fn commit(&self) -> committable::Commitment<Self> {
237        let mut builder = RawCommitmentBuilder::new(&Self::tag());
238
239        for (_, validator) in self.validators.iter().sorted_by_key(|(a, _)| *a) {
240            builder = builder.field("validator", validator.commit());
241        }
242
243        builder = builder.constant_str("used_bls_keys");
244        for key in self.used_bls_keys.iter().sorted() {
245            builder = builder.var_size_bytes(&key.to_bytes());
246        }
247
248        builder = builder.constant_str("used_schnorr_keys");
249        for key in self
250            .used_schnorr_keys
251            .iter()
252            .sorted_by(|a, b| a.to_affine().xy().cmp(&b.to_affine().xy()))
253        {
254            let mut schnorr_key_bytes = vec![];
255            key.serialize_with_mode(&mut schnorr_key_bytes, ark_serialize::Compress::Yes)
256                .unwrap();
257            builder = builder.var_size_bytes(&schnorr_key_bytes);
258        }
259
260        // Only include used_x25519_keys when non-empty to preserve the pre-fast-finality commitment
261        // value for stake tables that have no x25519 keys yet. Same pattern as x25519_key /
262        // p2p_addr on RegisteredValidator. Backward compatibility is cross-checked by
263        // REFERENCE_V4_HEADER_COMMITMENT in reference_tests.rs.
264        if !self.used_x25519_keys.is_empty() {
265            builder = builder.constant_str("used_x25519_keys");
266            for key in self.used_x25519_keys.iter().sorted() {
267                builder = builder.var_size_bytes(key.as_slice());
268            }
269        }
270
271        builder = builder.constant_str("validator_exits");
272
273        for key in self.validator_exits.iter().sorted() {
274            builder = builder.fixed_size_bytes(&key.into_array());
275        }
276
277        builder.finalize()
278    }
279
280    fn tag() -> String {
281        "STAKE_TABLE".to_string()
282    }
283}
284
285impl StakeTableState {
286    pub fn new(
287        validators: RegisteredValidatorMap,
288        validator_exits: HashSet<Address>,
289        used_bls_keys: HashSet<BLSPubKey>,
290        used_schnorr_keys: HashSet<SchnorrPubKey>,
291        used_x25519_keys: HashSet<x25519::PublicKey>,
292    ) -> Self {
293        Self {
294            validators,
295            validator_exits,
296            used_bls_keys,
297            used_schnorr_keys,
298            used_x25519_keys,
299        }
300    }
301
302    pub fn validators(&self) -> &RegisteredValidatorMap {
303        &self.validators
304    }
305
306    pub fn into_validators(self) -> RegisteredValidatorMap {
307        self.validators
308    }
309
310    pub fn used_bls_keys(&self) -> &HashSet<BLSPubKey> {
311        &self.used_bls_keys
312    }
313
314    pub fn used_schnorr_keys(&self) -> &HashSet<SchnorrPubKey> {
315        &self.used_schnorr_keys
316    }
317
318    pub fn used_x25519_keys(&self) -> &HashSet<x25519::PublicKey> {
319        &self.used_x25519_keys
320    }
321
322    pub fn validator_exits(&self) -> &HashSet<Address> {
323        &self.validator_exits
324    }
325
326    /// Applies a stake table event to this state.
327    ///
328    ///
329    /// This function MUST NOT modify `self` if the event is invalid. All validation
330    /// checks must be performed before any state modifications occur.
331    pub fn apply_event(&mut self, event: StakeTableEvent) -> ApplyEventResult<()> {
332        match event {
333            StakeTableEvent::Register(ValidatorRegistered {
334                account,
335                blsVk,
336                schnorrVk,
337                commission,
338            }) => {
339                let stake_table_key = BLSPubKey::try_from(blsVk).ok();
340                if stake_table_key.is_none() {
341                    tracing::warn!(
342                        ?account,
343                        "registering unauthenticated validator (invalid BLS key)"
344                    );
345                }
346                let state_ver_key = SchnorrPubKey::try_from(schnorrVk).ok();
347                if state_ver_key.is_none() {
348                    tracing::warn!(
349                        ?account,
350                        "registering unauthenticated validator (invalid Schnorr key)"
351                    );
352                }
353                let authenticated = stake_table_key.is_some() && state_ver_key.is_some();
354
355                if self.validator_exits.contains(&account) {
356                    return Err(StakeTableError::ValidatorAlreadyExited(account));
357                }
358
359                let entry = self.validators.entry(account);
360                if let indexmap::map::Entry::Occupied(_) = entry {
361                    return Err(StakeTableError::AlreadyRegistered(account));
362                }
363
364                // Unparsable keys (`None`) are never deduped; the L1 contract
365                // dedups by raw G2 bytes so distinct registrations are unique
366                // on-chain regardless.
367                if let Some(k) = stake_table_key.as_ref()
368                    && self.used_bls_keys.contains(k)
369                {
370                    return Err(StakeTableError::BlsKeyAlreadyUsed(k.to_string()));
371                }
372
373                // The stake table v1 contract does *not* enforce that each schnorr key is only used once.
374                // Unparsable schnorr keys (`None`) are not deduped.
375                if let Some(k) = state_ver_key.as_ref()
376                    && self.used_schnorr_keys.contains(k)
377                {
378                    return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
379                        k.to_string(),
380                    )));
381                }
382
383                // All checks ok, applying changes
384                if let Some(k) = stake_table_key.as_ref() {
385                    self.used_bls_keys.insert(*k);
386                }
387                if let Some(k) = state_ver_key.as_ref() {
388                    self.used_schnorr_keys.insert(k.clone());
389                }
390
391                entry.or_insert(RegisteredValidator {
392                    account,
393                    stake_table_key,
394                    state_ver_key,
395                    stake: U256::ZERO,
396                    commission,
397                    delegators: HashMap::new(),
398                    authenticated,
399                    x25519_key: None,
400                    p2p_addr: None,
401                });
402            },
403
404            StakeTableEvent::RegisterV2(ref reg) => {
405                let (stake_table_key, state_ver_key, authenticated) = match reg.authenticate() {
406                    Ok((bls, schnorr)) => (Some(bls), Some(schnorr), true),
407                    Err(e) => {
408                        tracing::warn!(
409                            account = ?reg.account,
410                            %e,
411                            "registering unauthenticated validator",
412                        );
413                        (
414                            BLSPubKey::try_from(reg.blsVK).ok(),
415                            SchnorrPubKey::try_from(reg.schnorrVK).ok(),
416                            false,
417                        )
418                    },
419                };
420
421                let ValidatorRegisteredV2 {
422                    account,
423                    commission,
424                    ..
425                } = reg;
426
427                // Reject if validator already exited
428                if self.validator_exits.contains(account) {
429                    return Err(StakeTableError::ValidatorAlreadyExited(*account));
430                }
431
432                let entry = self.validators.entry(*account);
433                if let indexmap::map::Entry::Occupied(_) = entry {
434                    return Err(StakeTableError::AlreadyRegistered(*account));
435                }
436
437                // Unparsable keys aren't deduped, parsable ones are whether or not the signature verified.
438                if let Some(k) = stake_table_key.as_ref()
439                    && self.used_bls_keys.contains(k)
440                {
441                    return Err(StakeTableError::BlsKeyAlreadyUsed(k.to_string()));
442                }
443
444                // The stake table v2 contract enforces schnorr key is only used once.
445                if let Some(k) = state_ver_key.as_ref()
446                    && self.used_schnorr_keys.contains(k)
447                {
448                    return Err(StakeTableError::SchnorrKeyAlreadyUsed(k.to_string()));
449                }
450
451                // All checks ok, applying changes
452                if let Some(k) = stake_table_key.as_ref() {
453                    self.used_bls_keys.insert(*k);
454                }
455                if let Some(k) = state_ver_key.as_ref() {
456                    self.used_schnorr_keys.insert(k.clone());
457                }
458
459                entry.or_insert(RegisteredValidator {
460                    account: *account,
461                    stake_table_key,
462                    state_ver_key,
463                    stake: U256::ZERO,
464                    commission: *commission,
465                    delegators: HashMap::new(),
466                    authenticated,
467                    x25519_key: None,
468                    p2p_addr: None,
469                });
470            },
471
472            StakeTableEvent::Deregister(ValidatorExit { validator })
473            | StakeTableEvent::DeregisterV2(ValidatorExitV2 { validator, .. }) => {
474                if !self.validators.contains_key(&validator) {
475                    return Err(StakeTableError::ValidatorNotFound(validator));
476                }
477
478                // All checks ok, applying changes
479                self.validator_exits.insert(validator);
480                self.validators.shift_remove(&validator);
481            },
482
483            StakeTableEvent::Delegate(delegated) => {
484                let Delegated {
485                    delegator,
486                    validator,
487                    amount,
488                } = delegated;
489
490                // Check amount is not zero first
491                if amount.is_zero() {
492                    return Err(StakeTableError::ZeroDelegatorStake(delegator));
493                }
494
495                let val = self
496                    .validators
497                    .get_mut(&validator)
498                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
499
500                // All checks ok, applying changes
501                // This cannot overflow in practice
502                val.stake = val.stake.checked_add(amount).unwrap_or_else(|| {
503                    panic!(
504                        "validator stake overflow: validator={validator}, stake={}, \
505                         amount={amount}",
506                        val.stake
507                    )
508                });
509                // Insert the delegator with the given stake
510                // or increase the stake if already present
511                val.delegators
512                    .entry(delegator)
513                    .and_modify(|stake| {
514                        *stake = stake.checked_add(amount).unwrap_or_else(|| {
515                            panic!(
516                                "delegator stake overflow: delegator={delegator}, stake={stake}, \
517                                 amount={amount}"
518                            )
519                        });
520                    })
521                    .or_insert(amount);
522            },
523
524            StakeTableEvent::Undelegate(Undelegated {
525                delegator,
526                validator,
527                amount,
528            })
529            | StakeTableEvent::UndelegateV2(UndelegatedV2 {
530                delegator,
531                validator,
532                amount,
533                ..
534            }) => {
535                let val = self
536                    .validators
537                    .get_mut(&validator)
538                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
539
540                if val.stake < amount {
541                    tracing::warn!("validator_stake={}, amount={amount}", val.stake);
542                    return Err(StakeTableError::InsufficientStake);
543                }
544
545                let delegator_stake = val
546                    .delegators
547                    .get_mut(&delegator)
548                    .ok_or(StakeTableError::DelegatorNotFound(delegator))?;
549
550                if *delegator_stake < amount {
551                    tracing::warn!("delegator_stake={delegator_stake}, amount={amount}");
552                    return Err(StakeTableError::InsufficientStake);
553                }
554
555                // Can unwrap because check above passed
556                let new_delegator_stake = delegator_stake.checked_sub(amount).unwrap();
557
558                // Can unwrap because check above passed
559                // All checks ok, applying changes
560                val.stake = val.stake.checked_sub(amount).unwrap();
561
562                if new_delegator_stake.is_zero() {
563                    val.delegators.remove(&delegator);
564                } else {
565                    *delegator_stake = new_delegator_stake;
566                }
567            },
568
569            StakeTableEvent::KeyUpdate(update) => {
570                let ConsensusKeysUpdated {
571                    account,
572                    blsVK,
573                    schnorrVK,
574                } = update;
575
576                let Ok(stake_table_key) = BLSPubKey::try_from(blsVK) else {
577                    return Ok(Err(ExpectedStakeTableError::InvalidBlsKey));
578                };
579                let Ok(state_ver_key) = SchnorrPubKey::try_from(schnorrVK) else {
580                    return Ok(Err(ExpectedStakeTableError::InvalidSchnorrKey));
581                };
582
583                if !self.validators.contains_key(&account) {
584                    return Err(StakeTableError::ValidatorNotFound(account));
585                }
586
587                if self.used_bls_keys.contains(&stake_table_key) {
588                    return Err(StakeTableError::BlsKeyAlreadyUsed(
589                        stake_table_key.to_string(),
590                    ));
591                }
592
593                // The stake table v1 contract does *not* enforce that each schnorr key is only used once,
594                // therefore it's possible to have multiple validators with the same schnorr key.
595                if self.used_schnorr_keys.contains(&state_ver_key) {
596                    return Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(
597                        state_ver_key.to_string(),
598                    )));
599                }
600
601                // All checks ok, applying changes
602                self.used_bls_keys.insert(stake_table_key);
603                self.used_schnorr_keys.insert(state_ver_key.clone());
604                // Can unwrap because check above passed
605                let validator = self.validators.get_mut(&account).unwrap_or_else(|| {
606                    panic!("validator {account} must exist after contains_key check")
607                });
608                validator.stake_table_key = Some(stake_table_key);
609                validator.state_ver_key = Some(state_ver_key);
610                // V1 contract verified BLS POP at submission; no off-chain sig to re-check.
611                validator.authenticated = true;
612            },
613
614            StakeTableEvent::KeyUpdateV2(update) => {
615                // `validate_event` pre-filters auth failures during live fetch;
616                // on replay an unparsable key is a soft skip, other failures are fatal.
617                let (stake_table_key, state_ver_key) = match update.authenticate() {
618                    Ok(parsed) => parsed,
619                    Err(StakeTableSolError::InvalidBlsKey) => {
620                        return Ok(Err(ExpectedStakeTableError::InvalidBlsKey));
621                    },
622                    Err(StakeTableSolError::InvalidSchnorrKey) => {
623                        return Ok(Err(ExpectedStakeTableError::InvalidSchnorrKey));
624                    },
625                    Err(e) => {
626                        return Err(StakeTableError::AuthenticationFailed(e.to_string()));
627                    },
628                };
629
630                let ConsensusKeysUpdatedV2 { account, .. } = update;
631
632                if !self.validators.contains_key(&account) {
633                    return Err(StakeTableError::ValidatorNotFound(account));
634                }
635
636                // The stake table contract enforces that each bls key is only used once.
637                if self.used_bls_keys.contains(&stake_table_key) {
638                    return Err(StakeTableError::BlsKeyAlreadyUsed(
639                        stake_table_key.to_string(),
640                    ));
641                }
642
643                // The stake table v2 contract enforces that each schnorr key is only used once
644                if self.used_schnorr_keys.contains(&state_ver_key) {
645                    return Err(StakeTableError::SchnorrKeyAlreadyUsed(
646                        state_ver_key.to_string(),
647                    ));
648                }
649
650                // All checks ok, applying changes
651                self.used_bls_keys.insert(stake_table_key);
652                self.used_schnorr_keys.insert(state_ver_key.clone());
653
654                // Can unwrap because check above passed
655                let validator = self.validators.get_mut(&account).unwrap_or_else(|| {
656                    panic!("validator {account} must exist after contains_key check")
657                });
658                validator.stake_table_key = Some(stake_table_key);
659                validator.state_ver_key = Some(state_ver_key);
660                validator.authenticated = true;
661            },
662
663            StakeTableEvent::CommissionUpdate(CommissionUpdated {
664                validator,
665                newCommission,
666                ..
667            }) => {
668                // NOTE: Commission update events are supported only in protocol
669                // version V4 and stake table contract V2.
670                if newCommission > COMMISSION_BASIS_POINTS {
671                    return Err(StakeTableError::InvalidCommission(validator, newCommission));
672                }
673
674                // NOTE: currently we are not enforcing changes to the
675                // commission increase rates and leave this enforcement to the
676                // stake table contract.
677                let val = self
678                    .validators
679                    .get_mut(&validator)
680                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
681                val.commission = newCommission;
682            },
683
684            StakeTableEvent::RegisterV3(ref reg) => {
685                let (stake_table_key, state_ver_key, authenticated) = match reg.authenticate() {
686                    Ok((bls, schnorr)) => (Some(bls), Some(schnorr), true),
687                    Err(e) => {
688                        tracing::warn!(
689                            account = ?reg.account,
690                            %e,
691                            "registering unauthenticated validator",
692                        );
693                        (
694                            BLSPubKey::try_from(reg.blsVK).ok(),
695                            SchnorrPubKey::try_from(reg.schnorrVK).ok(),
696                            false,
697                        )
698                    },
699                };
700
701                let ValidatorRegisteredV3 {
702                    account,
703                    commission,
704                    x25519Key,
705                    p2pAddr,
706                    ..
707                } = reg;
708
709                let x25519_key = parse_x25519_key(x25519Key.0)
710                    .inspect_err(|e| tracing::warn!(%e, ?account, "Invalid x25519 key"))
711                    .ok();
712
713                // Parse p2p addr
714                let p2p_addr = match p2pAddr.parse::<NetAddr>() {
715                    Ok(addr) => Some(addr),
716                    Err(e) => {
717                        if !p2pAddr.is_empty() {
718                            tracing::warn!(%e, account = ?account, "Failed to parse p2p addr");
719                        }
720                        None
721                    },
722                };
723
724                if self.validator_exits.contains(account) {
725                    return Err(StakeTableError::ValidatorAlreadyExited(*account));
726                }
727
728                let entry = self.validators.entry(*account);
729                if let indexmap::map::Entry::Occupied(_) = entry {
730                    return Err(StakeTableError::AlreadyRegistered(*account));
731                }
732
733                // Unparsable keys aren't deduped; the L1 contract dedups by raw G2 bytes.
734                if let Some(k) = stake_table_key.as_ref()
735                    && self.used_bls_keys.contains(k)
736                {
737                    return Err(StakeTableError::BlsKeyAlreadyUsed(k.to_string()));
738                }
739
740                if let Some(k) = state_ver_key.as_ref()
741                    && self.used_schnorr_keys.contains(k)
742                {
743                    return Err(StakeTableError::SchnorrKeyAlreadyUsed(k.to_string()));
744                }
745
746                if let Some(k) = x25519_key.as_ref()
747                    && self.used_x25519_keys.contains(k)
748                {
749                    return Err(StakeTableError::X25519KeyAlreadyUsed(k.to_string()));
750                }
751
752                // All checks ok, applying changes
753                if let Some(k) = stake_table_key.as_ref() {
754                    self.used_bls_keys.insert(*k);
755                }
756                if let Some(k) = state_ver_key.as_ref() {
757                    self.used_schnorr_keys.insert(k.clone());
758                }
759                if let Some(k) = x25519_key.as_ref() {
760                    self.used_x25519_keys.insert(*k);
761                }
762
763                entry.or_insert(RegisteredValidator {
764                    account: *account,
765                    stake_table_key,
766                    state_ver_key,
767                    stake: U256::ZERO,
768                    commission: *commission,
769                    delegators: HashMap::new(),
770                    authenticated,
771                    x25519_key,
772                    p2p_addr,
773                });
774            },
775
776            StakeTableEvent::X25519KeyUpdate(X25519KeyUpdated {
777                validator,
778                x25519Key,
779            }) => {
780                let val = self
781                    .validators
782                    .get_mut(&validator)
783                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
784
785                let x25519_key = parse_x25519_key(x25519Key.0)
786                    .inspect_err(|e| tracing::warn!(%e, ?validator, "Invalid x25519 key"))
787                    .ok();
788
789                if let Some(key) = x25519_key {
790                    if self.used_x25519_keys.contains(&key) {
791                        return Err(StakeTableError::X25519KeyAlreadyUsed(key.to_string()));
792                    }
793                    self.used_x25519_keys.insert(key);
794                }
795                val.x25519_key = x25519_key;
796            },
797
798            StakeTableEvent::P2pAddrUpdate(P2pAddrUpdated {
799                validator,
800                ref p2pAddr,
801            }) => {
802                let val = self
803                    .validators
804                    .get_mut(&validator)
805                    .ok_or(StakeTableError::ValidatorNotFound(validator))?;
806
807                match p2pAddr.parse::<NetAddr>() {
808                    Ok(addr) => val.p2p_addr = Some(addr),
809                    Err(e) => {
810                        tracing::warn!(%e, validator = ?validator, "Failed to parse p2p addr");
811                        val.p2p_addr = None;
812                    },
813                }
814            },
815        }
816
817        Ok(Ok(()))
818    }
819}
820
821/// The contract enforces canonical nonzero encodings, so an error here means a
822/// contract bug. Callers degrade it to `None` instead of aborting (defense in
823/// depth): the key only affects p2p networking, not consensus safety, so this
824/// is preferred over halting stake table processing on every node. The zero
825/// check is explicit because the Rust parser accepts the zero encoding.
826fn parse_x25519_key(bytes: [u8; 32]) -> anyhow::Result<x25519::PublicKey> {
827    ensure!(bytes != [0u8; 32], "zero key");
828    Ok(x25519::PublicKey::try_from(bytes.as_slice())?)
829}
830
831pub fn validators_from_l1_events<I: Iterator<Item = StakeTableEvent>>(
832    events: I,
833) -> Result<(RegisteredValidatorMap, StakeTableHash), StakeTableError> {
834    let mut state = StakeTableState::default();
835    for event in events {
836        match state.apply_event(event.clone()) {
837            Ok(Ok(())) => {
838                // Event successfully applied
839            },
840            Ok(Err(expected_err)) => {
841                // Expected error, dont change the state
842                tracing::warn!("Expected error while applying event {event:?}: {expected_err}");
843            },
844            Err(err) => {
845                tracing::error!("Fatal error in applying event {event:?}: {err}");
846                return Err(err);
847            },
848        }
849    }
850    let commit = state.commit();
851    Ok((state.into_validators(), commit))
852}
853
854/// Select active validators
855///
856/// Filters out unauthenticated validator candidates, those without stake, and selects
857/// the top [`MAX_VALIDATORS`] staked validators.
858/// Returns a new AuthenticatedValidatorMap containing only the selected validators.
859pub(crate) fn select_active_validator_set(
860    candidates: &RegisteredValidatorMap,
861    protocol_version: Version,
862) -> Result<AuthenticatedValidatorMap, StakeTableError> {
863    let total_candidates = candidates.len();
864
865    let valid_validators: AuthenticatedValidatorMap = candidates
866        .iter()
867        .filter_map(
868            |(address, validator)| match AuthenticatedValidator::try_from(validator) {
869                Err(e) => {
870                    tracing::debug!("{e}");
871                    None
872                },
873                Ok(cv) => {
874                    if cv.delegators.is_empty() {
875                        tracing::debug!("Validator {address:?} does not have any delegator");
876                        return None;
877                    }
878                    if cv.stake.is_zero() {
879                        tracing::debug!("Validator {address:?} does not have any stake");
880                        return None;
881                    }
882                    if !cv.is_eligible(protocol_version) {
883                        tracing::debug!(
884                            ?address,
885                            has_x25519 = cv.x25519_key.is_some(),
886                            has_p2p = cv.p2p_addr.is_some(),
887                            "Validator not eligible at protocol version {protocol_version}"
888                        );
889                        return None;
890                    }
891                    Some((*address, cv))
892                },
893            },
894        )
895        .collect();
896
897    tracing::debug!(
898        total_candidates,
899        filtered = valid_validators.len(),
900        "Filtered out invalid validators"
901    );
902
903    if valid_validators.is_empty() {
904        tracing::warn!("Validator selection failed: no validators passed minimum criteria");
905        return Err(StakeTableError::NoValidValidators);
906    }
907
908    let maximum_stake = valid_validators.values().map(|v| v.stake).max().unwrap();
909
910    let minimum_stake = maximum_stake
911        .checked_div(U256::from(VID_TARGET_TOTAL_STAKE))
912        .ok_or_else(|| {
913            tracing::error!("Overflow while calculating minimum stake threshold");
914            StakeTableError::MinimumStakeOverflow
915        })?;
916
917    let mut valid_stakers: Vec<_> = valid_validators
918        .iter()
919        .filter(|(_, v)| v.stake >= minimum_stake)
920        .map(|(addr, v)| (*addr, v.stake))
921        .collect();
922
923    tracing::info!(
924        count = valid_stakers.len(),
925        "Number of validators above minimum stake threshold"
926    );
927
928    // Sort by stake (descending order)
929    valid_stakers.sort_by_key(|(_, stake)| std::cmp::Reverse(*stake));
930
931    if valid_stakers.len() > MAX_VALIDATORS {
932        valid_stakers.truncate(MAX_VALIDATORS);
933    }
934
935    let selected_addresses: HashSet<_> = valid_stakers.iter().map(|(addr, _)| *addr).collect();
936    let selected_validators: AuthenticatedValidatorMap = valid_validators
937        .into_iter()
938        .filter(|(address, _)| selected_addresses.contains(address))
939        .collect();
940
941    tracing::info!(
942        final_count = selected_validators.len(),
943        "Selected active validator set"
944    );
945
946    Ok(selected_validators)
947}
948
949#[derive(Clone, Debug)]
950pub struct ValidatorSet {
951    pub(crate) all_validators: RegisteredValidatorMap,
952    pub(crate) active_validators: AuthenticatedValidatorMap,
953    pub(crate) stake_table_hash: Option<StakeTableHash>,
954    /// The protocol version at which `active_validators` was selected.
955    pub(crate) protocol_version: Version,
956}
957
958impl ValidatorSet {
959    /// Derive a validator set from a stake-table state at a given protocol version.
960    pub fn from_state(
961        state: &StakeTableState,
962        protocol_version: Version,
963    ) -> Result<Self, StakeTableError> {
964        let active_validators = select_active_validator_set(state.validators(), protocol_version)?;
965        Ok(Self {
966            all_validators: state.validators().clone(),
967            active_validators,
968            stake_table_hash: Some(state.commit()),
969            protocol_version,
970        })
971    }
972
973    /// Derive a validator set directly from L1 events.
974    pub fn from_l1_events<I: Iterator<Item = StakeTableEvent>>(
975        events: I,
976        protocol_version: Version,
977    ) -> Result<Self, StakeTableError> {
978        let (all_validators, stake_table_hash) = validators_from_l1_events(events)?;
979        let active_validators = select_active_validator_set(&all_validators, protocol_version)?;
980        Ok(Self {
981            all_validators,
982            active_validators,
983            stake_table_hash: Some(stake_table_hash),
984            protocol_version,
985        })
986    }
987
988    /// All registered validators known when this set was derived.
989    pub fn all_validators(&self) -> &RegisteredValidatorMap {
990        &self.all_validators
991    }
992
993    /// Validators selected to participate in consensus at `protocol_version`.
994    pub fn active_validators(&self) -> &AuthenticatedValidatorMap {
995        &self.active_validators
996    }
997
998    /// Commitment of the underlying stake-table state, if known.
999    pub fn stake_table_hash(&self) -> Option<StakeTableHash> {
1000        self.stake_table_hash
1001    }
1002
1003    /// Protocol version at which the active set was selected.
1004    pub fn protocol_version(&self) -> Version {
1005        self.protocol_version
1006    }
1007}
1008
1009impl std::fmt::Debug for StakeTableEvent {
1010    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1011        match self {
1012            StakeTableEvent::Register(event) => write!(f, "Register({:?})", event.account),
1013            StakeTableEvent::RegisterV2(event) => write!(f, "RegisterV2({:?})", event.account),
1014            StakeTableEvent::Deregister(event) => write!(f, "Deregister({:?})", event.validator),
1015            StakeTableEvent::DeregisterV2(event) => {
1016                write!(f, "DeregisterV2({:?})", event.validator)
1017            },
1018            StakeTableEvent::Delegate(event) => write!(f, "Delegate({:?})", event.delegator),
1019            StakeTableEvent::Undelegate(event) => write!(f, "Undelegate({:?})", event.delegator),
1020            StakeTableEvent::UndelegateV2(event) => {
1021                write!(f, "UndelegateV2({:?})", event.delegator)
1022            },
1023            StakeTableEvent::KeyUpdate(event) => write!(f, "KeyUpdate({:?})", event.account),
1024            StakeTableEvent::KeyUpdateV2(event) => write!(f, "KeyUpdateV2({:?})", event.account),
1025            StakeTableEvent::CommissionUpdate(event) => {
1026                write!(f, "CommissionUpdate({:?})", event.validator)
1027            },
1028            StakeTableEvent::RegisterV3(event) => {
1029                write!(f, "RegisterV3({:?})", event.account)
1030            },
1031            StakeTableEvent::X25519KeyUpdate(event) => {
1032                write!(f, "X25519KeyUpdate({:?})", event.validator)
1033            },
1034            StakeTableEvent::P2pAddrUpdate(event) => {
1035                write!(f, "P2pAddrUpdate({:?})", event.validator)
1036            },
1037        }
1038    }
1039}
1040
1041impl Fetcher {
1042    #[cfg(feature = "node")]
1043    pub fn new(
1044        peers: Arc<dyn StateCatchup>,
1045        persistence: Arc<AsyncMutex<dyn MembershipPersistence>>,
1046        l1_client: L1Client,
1047        chain_config: ChainConfig,
1048    ) -> Self {
1049        Self {
1050            peers,
1051            persistence,
1052            l1_client,
1053            chain_config: Arc::new(AsyncMutex::new(chain_config)),
1054            update_task: StakeTableUpdateTask(AsyncMutex::new(None)).into(),
1055            initial_supply: Arc::new(AsyncRwLock::new(None)),
1056        }
1057    }
1058
1059    #[cfg(feature = "node")]
1060    pub async fn spawn_update_loop(&self) {
1061        let mut update_task = self.update_task.0.lock().await;
1062        if update_task.is_none() {
1063            *update_task = Some(spawn(self.update_loop()));
1064        }
1065    }
1066
1067    /// Periodically updates the stake table from the L1 contract.
1068    /// This function polls the finalized block number from the L1 client at an interval
1069    /// and fetches stake table from contract
1070    /// and updates the persistence
1071    #[cfg(feature = "node")]
1072    fn update_loop(&self) -> impl Future<Output = ()> + use<> {
1073        let span = tracing::warn_span!("Stake table update loop");
1074        let self_clone = self.clone();
1075        let state = self.l1_client.state.clone();
1076        let l1_retry = self.l1_client.options().l1_retry_delay;
1077        let update_delay = self.l1_client.options().stake_table_update_interval;
1078        let chain_config = self.chain_config.clone();
1079
1080        async move {
1081            // Get the stake table contract address from the chain config.
1082            // This may not contain a stake table address if we are on a pre-epoch version.
1083            // It keeps retrying until the chain config is upgraded
1084            // after a successful upgrade to an epoch version.
1085            let stake_contract_address = loop {
1086                let contract = chain_config.lock().await.stake_table_contract;
1087                match contract {
1088                    Some(addr) => break addr,
1089                    None => {
1090                        tracing::debug!(
1091                            "Stake table contract address not found. Retrying in {l1_retry:?}...",
1092                        );
1093                    },
1094                }
1095                sleep(l1_retry).await;
1096            };
1097
1098            // Begin the main polling loop
1099            loop {
1100                let finalized_block = loop {
1101                    let last_finalized = state.lock().await.last_finalized;
1102                    if let Some(block) = last_finalized {
1103                        break block;
1104                    }
1105                    tracing::debug!("Finalized block not yet available. Retrying in {l1_retry:?}",);
1106                    sleep(l1_retry).await;
1107                };
1108
1109                tracing::debug!("Attempting to fetch stake table at L1 block {finalized_block:?}",);
1110
1111                loop {
1112                    match self_clone
1113                        .fetch_and_store_stake_table_events(stake_contract_address, finalized_block)
1114                        .await
1115                    {
1116                        Ok(events) => {
1117                            tracing::info!(
1118                                "Successfully fetched and stored stake table events at \
1119                                 block={finalized_block:?}"
1120                            );
1121                            tracing::debug!("events={events:?}");
1122                            break;
1123                        },
1124                        Err(e) => {
1125                            tracing::error!(
1126                                "Error fetching stake table at block {finalized_block:?}. err= \
1127                                 {e:#}",
1128                            );
1129                            sleep(l1_retry).await;
1130                        },
1131                    }
1132                }
1133
1134                tracing::debug!("Waiting {update_delay:?} before next stake table update...",);
1135                sleep(update_delay).await;
1136            }
1137        }
1138        .instrument(span)
1139    }
1140
1141    /// Get `StakeTable` at specific l1 block height.
1142    /// This function fetches and processes various events (ValidatorRegistered, ValidatorExit,
1143    /// Delegated, Undelegated, and ConsensusKeysUpdated) within the block range from the
1144    /// contract's initialization block to the provided `to_block` value.
1145    /// Events are fetched in chunks and retries are implemented for failed requests.
1146    /// Only new events fetched from L1 are stored in persistence.
1147    #[cfg(feature = "node")]
1148    pub async fn fetch_and_store_stake_table_events(
1149        &self,
1150        contract: Address,
1151        to_block: u64,
1152    ) -> anyhow::Result<Vec<(EventKey, StakeTableEvent)>> {
1153        let (read_l1_offset, persistence_events) = {
1154            let persistence_lock = self.persistence.lock().await;
1155            persistence_lock.load_events(0, to_block).await?
1156        };
1157
1158        tracing::info!("loaded events from storage to_block={to_block:?}");
1159
1160        // No need to fetch from contract
1161        // if persistence returns all the events that we need
1162        if let Some(EventsPersistenceRead::Complete) = read_l1_offset {
1163            return Ok(persistence_events);
1164        }
1165
1166        let from_block = read_l1_offset
1167            .map(|read| match read {
1168                EventsPersistenceRead::UntilL1Block(block) => Ok(block + 1),
1169                EventsPersistenceRead::Complete => Err(anyhow::anyhow!(
1170                    "Unexpected state. offset is complete after returning early"
1171                )),
1172            })
1173            .transpose()?;
1174
1175        ensure!(
1176            Some(to_block) >= from_block,
1177            "to_block {to_block:?} is less than from_block {from_block:?}"
1178        );
1179
1180        tracing::info!(%to_block, from_block = ?from_block, "Fetching events from contract");
1181
1182        let contract_events = Self::fetch_events_from_contract(
1183            self.l1_client.clone(),
1184            contract,
1185            from_block,
1186            to_block,
1187        )
1188        .await?;
1189
1190        // Store only the new events fetched from L1 contract
1191        tracing::info!(
1192            "storing {} new events in storage to_block={to_block:?}",
1193            contract_events.len()
1194        );
1195        {
1196            let persistence_lock = self.persistence.lock().await;
1197            persistence_lock
1198                .store_events(to_block, contract_events.clone())
1199                .await
1200                .inspect_err(|e| tracing::error!("failed to store events. err={e}"))?;
1201        }
1202
1203        let mut events = match from_block {
1204            Some(_) => persistence_events
1205                .into_iter()
1206                .chain(contract_events)
1207                .collect(),
1208            None => contract_events,
1209        };
1210
1211        // There are no duplicates because the RPC returns all events,
1212        // which are stored directly in persistence as is.
1213        // However, this step is taken as a precaution.
1214        // The vector is already sorted above, so this should be fast.
1215        let len_before_dedup = events.len();
1216        events.dedup();
1217        let len_after_dedup = events.len();
1218        if len_before_dedup != len_after_dedup {
1219            tracing::warn!("Duplicate events found and removed. This should not normally happen.")
1220        }
1221
1222        Ok(events)
1223    }
1224
1225    /// Validate a stake table event.
1226    ///
1227    /// Returns:
1228    /// - `Ok(true)` if the event is valid and should be processed
1229    /// - `Ok(false)` if the event should be skipped (non-fatal error)
1230    /// - `Err(StakeTableError)` if a fatal error occurs
1231    #[cfg_attr(not(feature = "node"), allow(dead_code))]
1232    fn validate_event(event: &StakeTableV3Events, log: &Log) -> Result<bool, StakeTableError> {
1233        match event {
1234            StakeTableV3Events::ConsensusKeysUpdatedV2(evt) => {
1235                if let Err(err) = evt.authenticate() {
1236                    tracing::warn!(
1237                        %err,
1238                        "Failed to authenticate ConsensusKeysUpdatedV2 event: {}",
1239                        log.display()
1240                    );
1241                    return Ok(false);
1242                }
1243            },
1244            StakeTableV3Events::CommissionUpdated(evt)
1245                if evt.newCommission > COMMISSION_BASIS_POINTS =>
1246            {
1247                return Err(StakeTableError::InvalidCommission(
1248                    evt.validator,
1249                    evt.newCommission,
1250                ));
1251            },
1252            _ => {},
1253        }
1254
1255        Ok(true)
1256    }
1257
1258    /// Break a block range into fixed-size chunks.
1259    #[cfg_attr(not(feature = "node"), allow(dead_code))]
1260    fn block_range_chunks(
1261        from_block: u64,
1262        to_block: u64,
1263        chunk_size: u64,
1264    ) -> impl Iterator<Item = (u64, u64)> {
1265        let mut start = from_block;
1266        let end = to_block;
1267        std::iter::from_fn(move || {
1268            let chunk_end = min(start + chunk_size - 1, end);
1269            if chunk_end < start {
1270                return None;
1271            }
1272            let chunk = (start, chunk_end);
1273            start = chunk_end + 1;
1274            Some(chunk)
1275        })
1276    }
1277
1278    /// Fetch all stake table events from L1
1279    #[cfg(feature = "node")]
1280    pub async fn fetch_events_from_contract(
1281        l1_client: L1Client,
1282        contract: Address,
1283        from_block: Option<u64>,
1284        to_block: u64,
1285    ) -> Result<Vec<(EventKey, StakeTableEvent)>, StakeTableError> {
1286        let stake_table_contract = StakeTableV3::new(contract, l1_client.provider.clone());
1287        let max_retry_duration = l1_client.options().l1_events_max_retry_duration;
1288        let retry_delay = l1_client.options().l1_retry_delay;
1289        // get the block number when the contract was initialized
1290        // to avoid fetching events from block number 0
1291        let from_block = match from_block {
1292            Some(block) => block,
1293            None => {
1294                let start = Instant::now();
1295                loop {
1296                    match stake_table_contract.initializedAtBlock().call().await {
1297                        Ok(init_block) => break init_block.to::<u64>(),
1298                        Err(err) => {
1299                            if start.elapsed() >= max_retry_duration {
1300                                panic!(
1301                                    "Failed to retrieve initial block after `{}`: {err}",
1302                                    format_duration(max_retry_duration)
1303                                );
1304                            }
1305                            tracing::warn!(%err, "Failed to retrieve initial block, retrying...");
1306                            sleep(retry_delay).await;
1307                        },
1308                    }
1309                }
1310            },
1311        };
1312
1313        // To avoid making large RPC calls, divide the range into smaller chunks.
1314        // chunk size is from env "ESPRESSO_L1_EVENTS_MAX_BLOCK_RANGE
1315        // default value  is `10000` if env variable is not set
1316        let chunk_size = l1_client.options().l1_events_max_block_range;
1317        let chunks = Self::block_range_chunks(from_block, to_block, chunk_size);
1318
1319        let mut events = vec![];
1320
1321        for (from, to) in chunks {
1322            let provider = l1_client.provider.clone();
1323
1324            tracing::debug!(from, to, "fetch all stake table events in range");
1325            // fetch events
1326            // retry if the call to the provider to fetch the events fails
1327            let logs: Vec<Log> = retry(
1328                retry_delay,
1329                max_retry_duration,
1330                "stake table events fetch",
1331                move || {
1332                    let provider = provider.clone();
1333
1334                    Box::pin(async move {
1335                        let filter = Filter::new()
1336                            .events([
1337                                ValidatorRegistered::SIGNATURE,
1338                                ValidatorRegisteredV2::SIGNATURE,
1339                                ValidatorRegisteredV3::SIGNATURE,
1340                                ValidatorExit::SIGNATURE,
1341                                ValidatorExitV2::SIGNATURE,
1342                                Delegated::SIGNATURE,
1343                                Undelegated::SIGNATURE,
1344                                UndelegatedV2::SIGNATURE,
1345                                ConsensusKeysUpdated::SIGNATURE,
1346                                ConsensusKeysUpdatedV2::SIGNATURE,
1347                                CommissionUpdated::SIGNATURE,
1348                                X25519KeyUpdated::SIGNATURE,
1349                                P2pAddrUpdated::SIGNATURE,
1350                            ])
1351                            .address(contract)
1352                            .from_block(from)
1353                            .to_block(to);
1354                        provider.get_logs(&filter).await
1355                    })
1356                },
1357            )
1358            .await;
1359
1360            let chunk_events = logs
1361                .into_iter()
1362                .filter_map(|log| {
1363                    let event =
1364                        StakeTableV3Events::decode_raw_log(log.topics(), &log.data().data).ok()?;
1365                    match Self::validate_event(&event, &log) {
1366                        Ok(true) => Some(Ok((event, log))),
1367                        Ok(false) => None,
1368                        Err(e) => Some(Err(e)),
1369                    }
1370                })
1371                .collect::<Result<Vec<_>, _>>()?;
1372
1373            events.extend(chunk_events);
1374        }
1375
1376        sort_stake_table_events(events).map_err(Into::into)
1377    }
1378
1379    // Only used by staking CLI which doesn't have persistence
1380    #[cfg(feature = "node")]
1381    pub async fn fetch_all_validators_from_contract(
1382        l1_client: L1Client,
1383        contract: Address,
1384        to_block: u64,
1385    ) -> anyhow::Result<(RegisteredValidatorMap, StakeTableHash)> {
1386        let events = Self::fetch_events_from_contract(l1_client, contract, None, to_block).await?;
1387
1388        // Process the sorted events and return the resulting stake table.
1389        validators_from_l1_events(events.into_iter().map(|(_, e)| e))
1390            .context("failed to construct validators set from l1 events")
1391    }
1392
1393    /// Returns the initial token supply, fetching it from L1 if not yet known.
1394    #[cfg(feature = "node")]
1395    pub async fn initial_supply_or_fetch(&self) -> Result<U256, FetchRewardError> {
1396        // `fetch_and_update_initial_supply` needs a write lock, create temporary to drop lock
1397        let supply = *self.initial_supply.read().await;
1398        match supply {
1399            Some(supply) => Ok(supply),
1400            None => self.fetch_and_update_initial_supply().await,
1401        }
1402    }
1403
1404    /// Returns the initial token supply, fetching it from L1 if not yet known.
1405    #[cfg(not(feature = "node"))]
1406    pub async fn initial_supply_or_fetch(&self) -> Result<U256, FetchRewardError> {
1407        Ok((*self.initial_supply.read().await).expect("initial supply pre-populated"))
1408    }
1409
1410    /// Calculates the fixed block reward based on the token's initial supply,
1411    /// obtained via `initial_supply_or_fetch`.
1412    #[cfg(feature = "node")]
1413    pub async fn fetch_fixed_block_reward(&self) -> Result<RewardAmount, FetchRewardError> {
1414        let initial_supply = self.initial_supply_or_fetch().await?;
1415
1416        let reward = ((initial_supply * U256::from(INFLATION_RATE)) / U256::from(BLOCKS_PER_YEAR))
1417            .checked_div(U256::from(COMMISSION_BASIS_POINTS))
1418            .ok_or(FetchRewardError::DivisionByZero(
1419                "COMMISSION_BASIS_POINTS is zero",
1420            ))?;
1421
1422        Ok(RewardAmount(reward))
1423    }
1424
1425    /// Fetches and updates the initial token supply.
1426    ///
1427    /// - Locates the `Initialized` event of the token contract (emitted only once).
1428    /// - Queries `Transfer` events in the same block, matching by transaction hash and
1429    ///   `from == address(0)` to find the initial mint.
1430    /// - If either step fails, the function aborts to prevent incorrect reward calculations.
1431    ///
1432    /// This avoids fetching transaction receipts, which may be unavailable on pruned L1 nodes.
1433    ///
1434    /// The ESP token contract itself does not expose the initialization block
1435    /// but the stake table contract does.
1436    /// The stake table contract is deployed after the token contract as it holds the token
1437    /// contract address. We use the stake table contract initialization block as a safe upper bound
1438    /// when scanning backwards for the token contract initialization event.
1439    #[cfg(feature = "node")]
1440    pub async fn fetch_and_update_initial_supply(&self) -> Result<U256, FetchRewardError> {
1441        tracing::info!("Fetching token initial supply");
1442        let chain_config = *self.chain_config.lock().await;
1443
1444        let stake_table_contract = chain_config
1445            .stake_table_contract
1446            .ok_or(FetchRewardError::MissingStakeTableContract)?;
1447
1448        let provider = self.l1_client.provider.clone();
1449        let stake_table = StakeTableV3::new(stake_table_contract, provider.clone());
1450
1451        // Get the block number where the stake table was initialized
1452        // Stake table contract has the token contract address
1453        // so the token contract is deployed before the stake table contract
1454        let stake_table_init_block = stake_table
1455            .initializedAtBlock()
1456            .block(BlockId::finalized())
1457            .call()
1458            .await
1459            .map_err(FetchRewardError::ContractCall)?
1460            .to::<u64>();
1461
1462        tracing::info!("stake table init block ={stake_table_init_block}");
1463
1464        let token_address = stake_table
1465            .token()
1466            .block(BlockId::finalized())
1467            .call()
1468            .await
1469            .map_err(FetchRewardError::TokenAddressFetch)?;
1470
1471        let token = EspToken::new(token_address, provider.clone());
1472
1473        // Fetch the `Initialized` event (emitted once during token contract init).
1474        // Falls back to scanning over a fixed block range if the full-range query fails.
1475        let init_logs = token
1476            .Initialized_filter()
1477            .from_block(0u64)
1478            .to_block(BlockNumberOrTag::Finalized)
1479            .query()
1480            .await;
1481
1482        let init_log = match init_logs {
1483            Ok(init_logs) => {
1484                if init_logs.is_empty() {
1485                    tracing::error!(
1486                        "Token Initialized event logs are empty. This should never happen"
1487                    );
1488                    return Err(FetchRewardError::MissingInitializedEvent);
1489                }
1490
1491                let (_, init_log) = init_logs[0].clone();
1492
1493                tracing::debug!(tx_hash = ?init_log.transaction_hash, "Found token `Initialized` event");
1494                init_log
1495            },
1496            Err(err) => {
1497                tracing::warn!(
1498                    "RPC returned error {err:?}. will fallback to scanning over fixed block range"
1499                );
1500                self.scan_token_contract_initialized_event_log(
1501                    stake_table_init_block,
1502                    token.clone(),
1503                )
1504                .await?
1505            },
1506        };
1507
1508        let init_block = init_log
1509            .block_number
1510            .ok_or(FetchRewardError::MissingBlockNumber)?;
1511
1512        let init_tx_hash =
1513            init_log
1514                .transaction_hash
1515                .ok_or_else(|| FetchRewardError::MissingTransactionHash {
1516                    init_log: init_log.clone().into(),
1517                })?;
1518
1519        // Query Transfer events in the initialization block instead of fetching
1520        // the transaction receipt, which pruned L1 nodes may not have.
1521        // Match by transaction hash to scope to the exact initialization tx.
1522        let transfer_logs = token
1523            .Transfer_filter()
1524            .from_block(init_block)
1525            .to_block(init_block)
1526            .query()
1527            .await
1528            .map_err(FetchRewardError::TransferEventQuery)?;
1529
1530        let (mint_transfer, _) = transfer_logs
1531            .iter()
1532            .find(|(transfer, log)| {
1533                log.transaction_hash == Some(init_tx_hash) && transfer.from == Address::ZERO
1534            })
1535            .ok_or(FetchRewardError::MissingTransferEvent)?;
1536
1537        tracing::debug!("mint transfer event ={mint_transfer:?}");
1538
1539        let initial_supply = mint_transfer.value;
1540
1541        tracing::info!("Initial token amount: {} ESP", format_ether(initial_supply));
1542
1543        let mut writer = self.initial_supply.write().await;
1544        *writer = Some(initial_supply);
1545
1546        Ok(initial_supply)
1547    }
1548
1549    /// Scans backwards in fixed-size block ranges to locate the `Initialized` event of the token contract.
1550    ///
1551    /// This is a fallback method used when querying the full block range for the `Initialized` event fails
1552    ///
1553    /// Starting from the stake table contract’s initialization block (which comes after the token contract
1554    /// is deployed), it scans in chunks (defined by `l1_events_max_block_range`) until it finds the event
1555    /// or until a maximum number of blocks (`MAX_BLOCKS_SCANNED`) is reached.
1556    #[cfg(feature = "node")]
1557    pub async fn scan_token_contract_initialized_event_log(
1558        &self,
1559        stake_table_init_block: u64,
1560        token: EspTokenInstance<L1Provider>,
1561    ) -> Result<Log, FetchRewardError> {
1562        let max_events_range = self.l1_client.options().l1_events_max_block_range;
1563        const MAX_BLOCKS_SCANNED: u64 = 200_000;
1564        let mut total_scanned = 0;
1565
1566        let mut from_block = stake_table_init_block.saturating_sub(max_events_range);
1567        let mut to_block = stake_table_init_block;
1568
1569        loop {
1570            if total_scanned >= MAX_BLOCKS_SCANNED {
1571                tracing::error!(
1572                    total_scanned,
1573                    "Exceeded maximum scan range while searching for token Initialized event"
1574                );
1575                return Err(FetchRewardError::ExceededMaxScanRange(MAX_BLOCKS_SCANNED));
1576            }
1577
1578            let init_logs = token
1579                .Initialized_filter()
1580                .from_block(from_block)
1581                .to_block(to_block)
1582                .query()
1583                .await
1584                .map_err(FetchRewardError::ScanQueryFailed)?;
1585
1586            if !init_logs.is_empty() {
1587                let (_, init_log) = init_logs[0].clone();
1588                tracing::info!(
1589                    from_block,
1590                    tx_hash = ?init_log.transaction_hash,
1591                    "Found token Initialized event during scan"
1592                );
1593                return Ok(init_log);
1594            }
1595
1596            total_scanned += max_events_range;
1597            from_block = from_block.saturating_sub(max_events_range);
1598            to_block = to_block.saturating_sub(max_events_range);
1599        }
1600    }
1601
1602    pub async fn update_chain_config(&self, header: &Header) -> anyhow::Result<()> {
1603        let chain_config = self.get_chain_config(header).await?;
1604        // update chain config
1605        *self.chain_config.lock().await = chain_config;
1606
1607        Ok(())
1608    }
1609
1610    #[cfg(feature = "node")]
1611    pub async fn fetch(&self, epoch: EpochNumber, header: &Header) -> anyhow::Result<ValidatorSet> {
1612        let chain_config = *self.chain_config.lock().await;
1613        let Some(address) = chain_config.stake_table_contract else {
1614            bail!("No stake table contract address found in Chain config");
1615        };
1616
1617        let Some(l1_finalized_block_info) = header.l1_finalized() else {
1618            bail!(
1619                "The epoch root for epoch {epoch} is missing the L1 finalized block info. This is \
1620                 a fatal error. Consensus is blocked and will not recover."
1621            );
1622        };
1623
1624        let events = match self
1625            .fetch_and_store_stake_table_events(address, l1_finalized_block_info.number())
1626            .await
1627            .map_err(GetStakeTablesError::L1ClientFetchError)
1628        {
1629            Ok(events) => events,
1630            Err(e) => {
1631                bail!("failed to fetch stake table events {e:?}");
1632            },
1633        };
1634
1635        // Selection runs at the epoch root header's protocol version: that header's
1636        // `next_stake_table_hash` was computed under the active-set rules in effect at the root,
1637        // so events spanning an upgrade are intentionally evaluated under the root's rules rather
1638        // than each event's wall-clock version.
1639        match ValidatorSet::from_l1_events(events.into_iter().map(|(_, e)| e), header.version()) {
1640            Ok(res) => Ok(res),
1641            Err(e) => {
1642                bail!("failed to construct stake table {e:?}");
1643            },
1644        }
1645    }
1646
1647    /// Retrieve and verify `ChainConfig`
1648    // TODO move to appropriate object (Header?)
1649    pub(crate) async fn get_chain_config(&self, header: &Header) -> anyhow::Result<ChainConfig> {
1650        let chain_config = self.chain_config.lock().await;
1651        let peers = self.peers.clone();
1652        let header_cf = header.chain_config();
1653        if chain_config.commit() == header_cf.commit() {
1654            return Ok(*chain_config);
1655        }
1656
1657        let cf = match header_cf.resolve() {
1658            Some(cf) => cf,
1659            None => peers
1660                .fetch_chain_config(header_cf.commit())
1661                .await
1662                .inspect_err(|err| {
1663                    tracing::error!("failed to get chain_config from peers. err: {err:?}");
1664                })?,
1665        };
1666
1667        Ok(cf)
1668    }
1669
1670    #[cfg(any(test, feature = "testing"))]
1671    pub fn mock() -> Self {
1672        use crate::{mock, v0_1::NoStorage};
1673        let chain_config = ChainConfig::default();
1674        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
1675            .expect("Failed to create L1 client");
1676
1677        let peers = Arc::new(mock::MockStateCatchup::default());
1678        let persistence = NoStorage;
1679
1680        Self::new(
1681            peers,
1682            Arc::new(AsyncMutex::new(persistence)),
1683            l1,
1684            chain_config,
1685        )
1686    }
1687}
1688
1689#[cfg_attr(not(feature = "node"), allow(dead_code))]
1690async fn retry<F, T, E>(
1691    retry_delay: Duration,
1692    max_duration: Duration,
1693    operation_name: &str,
1694    mut operation: F,
1695) -> T
1696where
1697    F: FnMut() -> BoxFuture<'static, Result<T, E>>,
1698    E: std::fmt::Display,
1699{
1700    let start = Instant::now();
1701    loop {
1702        match operation().await {
1703            Ok(result) => return result,
1704            Err(err) => {
1705                if start.elapsed() >= max_duration {
1706                    panic!(
1707                        r#"
1708                    Failed to complete operation `{operation_name}` after `{}`.
1709                    error: {err}
1710
1711
1712                    This might be caused by:
1713                    - The current block range being too large for your RPC provider.
1714                    - The event query returning more data than your RPC allows as
1715                      some RPC providers limit the number of events returned.
1716                    - RPC provider outage
1717
1718                    Suggested solution:
1719                    - Reduce the value of the environment variable
1720                      `ESPRESSO_L1_EVENTS_MAX_BLOCK_RANGE` to query smaller ranges.
1721                    - Add multiple RPC providers
1722                    - Use a different RPC provider with higher rate limits."#,
1723                        format_duration(max_duration)
1724                    );
1725                }
1726                tracing::warn!(%err, "Retrying `{operation_name}` after error");
1727                sleep(retry_delay).await;
1728            },
1729        }
1730    }
1731}
1732
1733/// Calculates the stake ratio `p` and reward rate `R(p)`.
1734///
1735/// The reward rate `R(p)` is defined as:
1736///
1737///     R(p) = {
1738///         0.03 / sqrt(2 * 0.01),         if 0 <= p <= 0.01
1739///         0.03 / sqrt(2 * p),            if 0.01 < p <= 1
1740///     }
1741///
1742pub fn calculate_proportion_staked_and_reward_rate(
1743    total_stake: &BigDecimal,
1744    total_supply: &BigDecimal,
1745) -> anyhow::Result<(BigDecimal, BigDecimal)> {
1746    if total_supply.is_zero() {
1747        return Err(anyhow::anyhow!("Total supply cannot be zero"));
1748    }
1749
1750    let proportion_staked = total_stake / total_supply;
1751
1752    if proportion_staked < BigDecimal::zero() || proportion_staked > BigDecimal::one() {
1753        return Err(anyhow::anyhow!("Stake ratio p must be in the range [0, 1]"));
1754    }
1755
1756    let two = BigDecimal::from_u32(2).unwrap();
1757    let min_stake_ratio = BigDecimal::from_str("0.01")?;
1758    let numerator = BigDecimal::from_str("0.03")?;
1759
1760    let denominator = (&two * (&proportion_staked).max(&min_stake_ratio))
1761        .sqrt()
1762        .context("Failed to compute sqrt in R(p)")?;
1763
1764    let reward_rate = numerator / denominator;
1765
1766    tracing::debug!("rp={reward_rate}");
1767
1768    Ok((proportion_staked, reward_rate))
1769}
1770
1771pub(crate) fn compute_block_reward(
1772    epoch: EpochNumber,
1773    total_supply: U256,
1774    total_stake: U256,
1775    avg_block_time_ms: u64,
1776) -> anyhow::Result<RewardAmount> {
1777    // Convert to BigDecimal for precision
1778    let total_stake_bd = BigDecimal::from_str(&total_stake.to_string())?;
1779    let total_supply_bd = BigDecimal::from_str(&(total_supply.to_string()))?;
1780
1781    tracing::debug!(?epoch, "total_stake={total_stake}");
1782    tracing::debug!(?epoch, "total_supply_bd={total_supply_bd}");
1783
1784    let (proportion, reward_rate) =
1785        calculate_proportion_staked_and_reward_rate(&total_stake_bd, &total_supply_bd)?;
1786    let inflation_rate = proportion * reward_rate;
1787
1788    tracing::debug!(?epoch, "inflation_rate={inflation_rate:?}");
1789
1790    let blocks_per_year = MILLISECONDS_PER_YEAR
1791        .checked_div(avg_block_time_ms.into())
1792        .context("avg_block_time_ms is zero")?;
1793
1794    tracing::debug!(?epoch, "blocks_per_year={blocks_per_year:?}");
1795
1796    ensure!(!blocks_per_year.is_zero(), "blocks per year is zero");
1797    let block_reward = (total_supply_bd * inflation_rate) / blocks_per_year;
1798
1799    let block_reward_u256 = U256::from_str(&block_reward.round(0).to_string())?;
1800
1801    Ok(block_reward_u256.into())
1802}
1803
1804#[derive(Error, Debug)]
1805/// Error representing fail cases for retrieving the stake table.
1806#[cfg_attr(not(feature = "node"), allow(dead_code))]
1807enum GetStakeTablesError {
1808    #[error("Error fetching from L1: {0}")]
1809    L1ClientFetchError(anyhow::Error),
1810}
1811
1812#[cfg(any(test, feature = "testing"))]
1813impl super::v0_3::StakeTable {
1814    /// Generate a `StakeTable` with `n` members.
1815    pub fn mock(n: u64) -> Self {
1816        use hotshot_types::PeerConfig;
1817
1818        use super::SeqTypes;
1819
1820        [..n]
1821            .iter()
1822            .map(|_| PeerConfig::test_default())
1823            .collect::<Vec<PeerConfig<SeqTypes>>>()
1824            .into()
1825    }
1826}
1827
1828#[cfg(any(test, feature = "testing"))]
1829impl DAMembers {
1830    /// Generate a `DaMembers` (alias committee) with `n` members.
1831    pub fn mock(n: u64) -> Self {
1832        use hotshot_types::PeerConfig;
1833
1834        use super::SeqTypes;
1835
1836        [..n]
1837            .iter()
1838            .map(|_| PeerConfig::test_default())
1839            .collect::<Vec<PeerConfig<SeqTypes>>>()
1840            .into()
1841    }
1842}
1843
1844#[cfg(any(test, feature = "testing"))]
1845pub mod testing {
1846    use alloy::primitives::Bytes;
1847    use hotshot_contract_adapter::{
1848        sol_types::{EdOnBN254PointSol, G1PointSol, G2PointSol},
1849        stake_table::{StateSignatureSol, sign_address_bls, sign_address_schnorr},
1850    };
1851    use hotshot_types::{light_client::StateKeyPair, signature_key::BLSKeyPair};
1852    use rand::{Rng as _, RngCore as _};
1853
1854    use super::*;
1855
1856    // TODO: current tests are just sanity checks, we need more.
1857
1858    #[derive(Debug, Clone)]
1859    pub struct TestValidator {
1860        pub account: Address,
1861        pub bls_vk: G2PointSol,
1862        pub schnorr_vk: EdOnBN254PointSol,
1863        pub commission: u16,
1864        pub bls_sig: G1PointSol,
1865        pub schnorr_sig: Bytes,
1866        pub x25519_key: [u8; 32],
1867        pub p2p_addr: String,
1868    }
1869
1870    impl TestValidator {
1871        pub fn random() -> Self {
1872            let account = Address::random();
1873            let commission = rand::thread_rng().gen_range(0..10000);
1874            Self::random_update_keys(account, commission)
1875        }
1876
1877        pub fn randomize_keys(&self) -> Self {
1878            Self::random_update_keys(self.account, self.commission)
1879        }
1880
1881        pub fn random_update_keys(account: Address, commission: u16) -> Self {
1882            let mut rng = &mut rand::thread_rng();
1883            let mut seed = [0u8; 32];
1884            rng.fill_bytes(&mut seed);
1885            let bls_key_pair = BLSKeyPair::generate(&mut rng);
1886            let bls_sig = sign_address_bls(&bls_key_pair, account);
1887            let schnorr_key_pair = StateKeyPair::generate_from_seed_indexed(seed, 0);
1888            let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, account);
1889            let mut x25519_key = [0u8; 32];
1890            rng.fill_bytes(&mut x25519_key);
1891            Self {
1892                account,
1893                bls_vk: bls_key_pair.ver_key().to_affine().into(),
1894                schnorr_vk: schnorr_key_pair.ver_key().to_affine().into(),
1895                commission,
1896                bls_sig: bls_sig.into(),
1897                schnorr_sig: StateSignatureSol::from(schnorr_sig).into(),
1898                x25519_key,
1899                p2p_addr: "127.0.0.1:9000".to_string(),
1900            }
1901        }
1902
1903        pub fn with_x25519_key(mut self, x25519_key: [u8; 32]) -> Self {
1904            self.x25519_key = x25519_key;
1905            self
1906        }
1907
1908        pub fn with_p2p_addr(mut self, p2p_addr: impl Into<String>) -> Self {
1909            self.p2p_addr = p2p_addr.into();
1910            self
1911        }
1912
1913        pub fn x25519_update(&self, x25519_key: [u8; 32]) -> StakeTableEvent {
1914            StakeTableEvent::X25519KeyUpdate(X25519KeyUpdated {
1915                validator: self.account,
1916                x25519Key: alloy::primitives::FixedBytes(x25519_key),
1917            })
1918        }
1919
1920        pub fn p2p_update(&self, p2p_addr: impl Into<String>) -> StakeTableEvent {
1921            StakeTableEvent::P2pAddrUpdate(P2pAddrUpdated {
1922                validator: self.account,
1923                p2pAddr: p2p_addr.into(),
1924            })
1925        }
1926    }
1927
1928    impl From<&TestValidator> for ValidatorRegistered {
1929        fn from(value: &TestValidator) -> Self {
1930            Self {
1931                account: value.account,
1932                blsVk: value.bls_vk,
1933                schnorrVk: value.schnorr_vk,
1934                commission: value.commission,
1935            }
1936        }
1937    }
1938
1939    impl From<&TestValidator> for ValidatorRegisteredV2 {
1940        fn from(value: &TestValidator) -> Self {
1941            Self {
1942                account: value.account,
1943                blsVK: value.bls_vk,
1944                schnorrVK: value.schnorr_vk,
1945                commission: value.commission,
1946                blsSig: value.bls_sig.into(),
1947                schnorrSig: value.schnorr_sig.clone(),
1948                metadataUri: "dummy-meta".to_string(),
1949            }
1950        }
1951    }
1952
1953    impl From<&TestValidator> for ValidatorRegisteredV3 {
1954        fn from(value: &TestValidator) -> Self {
1955            Self {
1956                account: value.account,
1957                blsVK: value.bls_vk,
1958                schnorrVK: value.schnorr_vk,
1959                commission: value.commission,
1960                blsSig: value.bls_sig.into(),
1961                schnorrSig: value.schnorr_sig.clone(),
1962                metadataUri: "dummy-meta".to_string(),
1963                x25519Key: alloy::primitives::FixedBytes(value.x25519_key),
1964                p2pAddr: value.p2p_addr.clone(),
1965            }
1966        }
1967    }
1968
1969    impl From<&TestValidator> for ConsensusKeysUpdated {
1970        fn from(value: &TestValidator) -> Self {
1971            Self {
1972                account: value.account,
1973                blsVK: value.bls_vk,
1974                schnorrVK: value.schnorr_vk,
1975            }
1976        }
1977    }
1978
1979    impl From<&TestValidator> for ConsensusKeysUpdatedV2 {
1980        fn from(value: &TestValidator) -> Self {
1981            Self {
1982                account: value.account,
1983                blsVK: value.bls_vk,
1984                schnorrVK: value.schnorr_vk,
1985                blsSig: value.bls_sig.into(),
1986                schnorrSig: value.schnorr_sig.clone(),
1987            }
1988        }
1989    }
1990
1991    impl From<&TestValidator> for ValidatorExit {
1992        fn from(value: &TestValidator) -> Self {
1993            Self {
1994                validator: value.account,
1995            }
1996        }
1997    }
1998
1999    impl RegisteredValidator<BLSPubKey> {
2000        pub fn mock() -> RegisteredValidator<BLSPubKey> {
2001            let val = TestValidator::random();
2002            let rng = &mut rand::thread_rng();
2003            let mut seed = [1u8; 32];
2004            rng.fill_bytes(&mut seed);
2005            let mut validator_stake = alloy::primitives::U256::from(0);
2006            let mut delegators = HashMap::new();
2007            for _i in 0..=5000 {
2008                let stake: u64 = rng.gen_range(0..10000);
2009                delegators.insert(Address::random(), alloy::primitives::U256::from(stake));
2010                validator_stake += alloy::primitives::U256::from(stake);
2011            }
2012
2013            let stake_table_key = BLSPubKey::try_from(val.bls_vk).expect("valid test BLS key");
2014            let state_ver_key =
2015                SchnorrPubKey::try_from(val.schnorr_vk).expect("valid test Schnorr key");
2016
2017            RegisteredValidator {
2018                account: val.account,
2019                stake_table_key: Some(stake_table_key),
2020                state_ver_key: Some(state_ver_key),
2021                stake: validator_stake,
2022                commission: val.commission,
2023                delegators,
2024                authenticated: true,
2025                x25519_key: None,
2026                p2p_addr: None,
2027            }
2028        }
2029    }
2030
2031    impl AuthenticatedValidator<BLSPubKey> {
2032        pub fn mock() -> AuthenticatedValidator<BLSPubKey> {
2033            RegisteredValidator::mock()
2034                .try_into()
2035                .expect("mock validator is always authenticated")
2036        }
2037
2038        pub fn mock_with_commission(commission: u16) -> AuthenticatedValidator<BLSPubKey> {
2039            let mut inner = RegisteredValidator::mock();
2040            inner.commission = commission;
2041            inner
2042                .try_into()
2043                .expect("mock validator is always authenticated")
2044        }
2045    }
2046}
2047
2048#[cfg(test)]
2049mod tests {
2050    use alloy::{
2051        primitives::{Address, Bytes},
2052        rpc::types::Log,
2053    };
2054    use hotshot_contract_adapter::{
2055        sol_types::{G1PointSol, G2PointSol},
2056        stake_table::{StakeTableContractVersion, sign_address_bls},
2057    };
2058    use hotshot_types::{light_client::StateKeyPair, signature_key::BLSKeyPair};
2059    use pretty_assertions::assert_matches;
2060    use rstest::rstest;
2061    use versions::{
2062        DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION, NEW_PROTOCOL_VERSION,
2063    };
2064
2065    use super::*;
2066    use crate::{L1ClientOptions, v0::impls::testing::*};
2067
2068    fn zero_g2() -> G2PointSol {
2069        G2PointSol {
2070            x0: U256::ZERO,
2071            x1: U256::ZERO,
2072            y0: U256::ZERO,
2073            y1: U256::ZERO,
2074        }
2075    }
2076
2077    fn zero_g1() -> G1PointSol {
2078        G1PointSol {
2079            x: U256::ZERO,
2080            y: U256::ZERO,
2081        }
2082    }
2083
2084    #[test_log::test]
2085    fn test_from_l1_events() -> anyhow::Result<()> {
2086        // Build a stake table with one DA node and one consensus node.
2087        let val_1 = TestValidator::random();
2088        let val_1_new_keys = val_1.randomize_keys();
2089        let val_2 = TestValidator::random();
2090        let val_2_new_keys = val_2.randomize_keys();
2091        let delegator = Address::random();
2092        let mut events: Vec<StakeTableEvent> = [
2093            ValidatorRegistered::from(&val_1).into(),
2094            ValidatorRegisteredV2::from(&val_2).into(),
2095            Delegated {
2096                delegator,
2097                validator: val_1.account,
2098                amount: U256::from(10),
2099            }
2100            .into(),
2101            ConsensusKeysUpdated::from(&val_1_new_keys).into(),
2102            ConsensusKeysUpdatedV2::from(&val_2_new_keys).into(),
2103            Undelegated {
2104                delegator,
2105                validator: val_1.account,
2106                amount: U256::from(7),
2107            }
2108            .into(),
2109            // delegate to the same validator again
2110            Delegated {
2111                delegator,
2112                validator: val_1.account,
2113                amount: U256::from(5),
2114            }
2115            .into(),
2116            // delegate to the second validator
2117            Delegated {
2118                delegator: Address::random(),
2119                validator: val_2.account,
2120                amount: U256::from(3),
2121            }
2122            .into(),
2123        ]
2124        .to_vec();
2125
2126        let validators_set = ValidatorSet::from_l1_events(events.iter().cloned(), EPOCH_VERSION)?;
2127        let st = validators_set.active_validators;
2128        let st_val_1 = st.get(&val_1.account).unwrap();
2129        // final staked amount should be 10 (delegated) - 7 (undelegated) + 5 (Delegated)
2130        assert_eq!(st_val_1.stake, U256::from(8));
2131        assert_eq!(st_val_1.commission, val_1.commission);
2132        assert_eq!(st_val_1.delegators.len(), 1);
2133        // final delegated amount should be 10 (delegated) - 7 (undelegated) + 5 (Delegated)
2134        assert_eq!(*st_val_1.delegators.get(&delegator).unwrap(), U256::from(8));
2135
2136        let st_val_2 = st.get(&val_2.account).unwrap();
2137        assert_eq!(st_val_2.stake, U256::from(3));
2138        assert_eq!(st_val_2.commission, val_2.commission);
2139        assert_eq!(st_val_2.delegators.len(), 1);
2140
2141        events.push(ValidatorExit::from(&val_1).into());
2142
2143        let validator_set = ValidatorSet::from_l1_events(events.iter().cloned(), EPOCH_VERSION)?;
2144        let st = validator_set.active_validators;
2145        // The first validator should have been removed
2146        assert_eq!(st.get(&val_1.account), None);
2147
2148        // The second validator should be unchanged
2149        let st_val_2 = st.get(&val_2.account).unwrap();
2150        assert_eq!(st_val_2.stake, U256::from(3));
2151        assert_eq!(st_val_2.commission, val_2.commission);
2152        assert_eq!(st_val_2.delegators.len(), 1);
2153
2154        // remove the 2nd validator
2155        events.push(ValidatorExit::from(&val_2).into());
2156
2157        // This should fail because the validator has exited and no longer exists in the stake table.
2158        assert!(ValidatorSet::from_l1_events(events.iter().cloned(), EPOCH_VERSION).is_err());
2159
2160        Ok(())
2161    }
2162
2163    #[test]
2164    fn test_from_l1_events_failures() -> anyhow::Result<()> {
2165        let val = TestValidator::random();
2166        let delegator = Address::random();
2167
2168        let register: StakeTableEvent = ValidatorRegistered::from(&val).into();
2169        let register_v2: StakeTableEvent = ValidatorRegisteredV2::from(&val).into();
2170        let delegate: StakeTableEvent = Delegated {
2171            delegator,
2172            validator: val.account,
2173            amount: U256::from(10),
2174        }
2175        .into();
2176        let key_update: StakeTableEvent = ConsensusKeysUpdated::from(&val).into();
2177        let key_update_v2: StakeTableEvent = ConsensusKeysUpdatedV2::from(&val).into();
2178        let undelegate: StakeTableEvent = Undelegated {
2179            delegator,
2180            validator: val.account,
2181            amount: U256::from(7),
2182        }
2183        .into();
2184
2185        let exit: StakeTableEvent = ValidatorExit::from(&val).into();
2186
2187        let cases = [
2188            vec![exit],
2189            vec![undelegate.clone()],
2190            vec![delegate.clone()],
2191            vec![key_update],
2192            vec![key_update_v2],
2193            vec![register.clone(), register.clone()],
2194            vec![register_v2.clone(), register_v2.clone()],
2195            vec![register.clone(), register_v2.clone()],
2196            vec![register_v2.clone(), register.clone()],
2197            vec![
2198                register,
2199                delegate.clone(),
2200                undelegate.clone(),
2201                undelegate.clone(),
2202            ],
2203            vec![register_v2, delegate, undelegate.clone(), undelegate],
2204        ];
2205
2206        for events in cases.iter() {
2207            // NOTE: not selecting the active validator set because we care about wrong sequences of
2208            // events being detected. If we compute the active set we will also get an error if the
2209            // set is empty but that's not what we want to test here.
2210            let res = validators_from_l1_events(events.iter().cloned());
2211            assert!(
2212                res.is_err(),
2213                "events {res:?}, not a valid sequence of events"
2214            );
2215        }
2216        Ok(())
2217    }
2218
2219    #[test]
2220    fn test_validators_selection() {
2221        let mut candidates = IndexMap::new();
2222        let mut highest_stake = alloy::primitives::U256::ZERO;
2223
2224        for _i in 0..3000 {
2225            let candidate = RegisteredValidator::mock();
2226            candidates.insert(candidate.account, candidate.clone());
2227
2228            if candidate.stake > highest_stake {
2229                highest_stake = candidate.stake;
2230            }
2231        }
2232
2233        let minimum_stake = highest_stake / U256::from(VID_TARGET_TOTAL_STAKE);
2234
2235        let selected_validators = select_active_validator_set(&candidates, EPOCH_VERSION)
2236            .expect("Failed to select validators");
2237        assert!(
2238            selected_validators.len() <= MAX_VALIDATORS,
2239            "validators len is {}, expected at most {MAX_VALIDATORS}",
2240            selected_validators.len()
2241        );
2242
2243        let mut selected_validators_highest_stake = alloy::primitives::U256::ZERO;
2244        // Ensure every validator in the final selection is above or equal to minimum stake
2245        for (address, validator) in &selected_validators {
2246            assert!(
2247                validator.stake >= minimum_stake,
2248                "Validator {:?} has stake below minimum: {}",
2249                address,
2250                validator.stake
2251            );
2252
2253            if validator.stake > selected_validators_highest_stake {
2254                selected_validators_highest_stake = validator.stake;
2255            }
2256        }
2257    }
2258
2259    // For a bug where the GCL did not match the stake table contract implementation and allowed
2260    // duplicated BLS keys via the update keys events.
2261    #[rstest::rstest]
2262    fn test_regression_non_unique_bls_keys_not_discarded(
2263        #[values(
2264            StakeTableContractVersion::V1,
2265            StakeTableContractVersion::V2,
2266            StakeTableContractVersion::V3
2267        )]
2268        version: StakeTableContractVersion,
2269    ) {
2270        let val = TestValidator::random();
2271        let register: StakeTableEvent = match version {
2272            StakeTableContractVersion::V1 => ValidatorRegistered::from(&val).into(),
2273            StakeTableContractVersion::V2 => ValidatorRegisteredV2::from(&val).into(),
2274            StakeTableContractVersion::V3 => StakeTableEvent::RegisterV3((&val).into()),
2275        };
2276        let delegate: StakeTableEvent = Delegated {
2277            delegator: Address::random(),
2278            validator: val.account,
2279            amount: U256::from(10),
2280        }
2281        .into();
2282
2283        // first ensure that wan build a valid stake table
2284        assert!(
2285            ValidatorSet::from_l1_events(
2286                vec![register.clone(), delegate.clone()].into_iter(),
2287                EPOCH_VERSION,
2288            )
2289            .is_ok()
2290        );
2291
2292        // add the invalid key update (re-using the same consensus keys)
2293        let key_update = ConsensusKeysUpdated::from(&val).into();
2294        let err = ValidatorSet::from_l1_events(
2295            vec![register, delegate, key_update].into_iter(),
2296            EPOCH_VERSION,
2297        )
2298        .unwrap_err();
2299
2300        let bls = BLSPubKey::try_from(val.bls_vk).expect("valid test BLS key");
2301        assert!(matches!(err, StakeTableError::BlsKeyAlreadyUsed(addr) if addr == bls.to_string()));
2302    }
2303
2304    // Test that the GCL does not
2305    // allow re-registration for the same Ethereum account.
2306    #[test]
2307    fn test_regression_reregister_eth_account() {
2308        let val1 = TestValidator::random();
2309        let val2 = val1.randomize_keys();
2310        let account = val1.account;
2311
2312        let register1 = ValidatorRegisteredV2::from(&val1).into();
2313        let deregister1 = ValidatorExit::from(&val1).into();
2314        let register2 = ValidatorRegisteredV2::from(&val2).into();
2315        let events = [register1, deregister1, register2];
2316        let error = validators_from_l1_events(events.iter().cloned()).unwrap_err();
2317        assert_matches!(error, StakeTableError::ValidatorAlreadyExited(addr) if addr == account);
2318    }
2319
2320    #[test]
2321    fn test_display_log() {
2322        let serialized = r#"{"address":"0x0000000000000000000000000000000000000069",
2323            "topics":["0x0000000000000000000000000000000000000000000000000000000000000069"],
2324            "data":"0x69",
2325            "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000069",
2326            "blockNumber":"0x69","blockTimestamp":"0x69",
2327            "transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000069",
2328            "transactionIndex":"0x69","logIndex":"0x70","removed":false}"#;
2329        let log: Log = serde_json::from_str(serialized).unwrap();
2330        assert_eq!(
2331            log.display(),
2332            "Log(block=105,index=112,\
2333             transaction_hash=0x0000000000000000000000000000000000000000000000000000000000000069)"
2334        )
2335    }
2336
2337    #[rstest]
2338    #[case::v1(StakeTableContractVersion::V1)]
2339    #[case::v2(StakeTableContractVersion::V2)]
2340    #[case::v3(StakeTableContractVersion::V3)]
2341    fn test_register_validator(#[case] version: StakeTableContractVersion) {
2342        let mut state = StakeTableState::default();
2343        let validator = TestValidator::random();
2344
2345        let event = match version {
2346            StakeTableContractVersion::V1 => StakeTableEvent::Register((&validator).into()),
2347            StakeTableContractVersion::V2 => StakeTableEvent::RegisterV2((&validator).into()),
2348            StakeTableContractVersion::V3 => StakeTableEvent::RegisterV3((&validator).into()),
2349        };
2350
2351        state.apply_event(event).unwrap().unwrap();
2352
2353        let stored = state.validators.get(&validator.account).unwrap();
2354        assert_eq!(stored.account, validator.account);
2355    }
2356
2357    #[rstest]
2358    #[case::v1(StakeTableContractVersion::V1)]
2359    #[case::v2(StakeTableContractVersion::V2)]
2360    #[case::v3(StakeTableContractVersion::V3)]
2361    fn test_validator_already_registered(#[case] version: StakeTableContractVersion) {
2362        let mut stake_table_state = StakeTableState::default();
2363
2364        let test_validator = TestValidator::random();
2365
2366        // First registration attempt using the specified contract version
2367        match version {
2368            StakeTableContractVersion::V1 => {
2369                stake_table_state.apply_event(StakeTableEvent::Register((&test_validator).into()))
2370            },
2371            StakeTableContractVersion::V2 => {
2372                stake_table_state.apply_event(StakeTableEvent::RegisterV2((&test_validator).into()))
2373            },
2374            StakeTableContractVersion::V3 => {
2375                stake_table_state.apply_event(StakeTableEvent::RegisterV3((&test_validator).into()))
2376            },
2377        }
2378        .unwrap()
2379        .unwrap(); // Expect the first registration to succeed
2380
2381        // attempt using V1 registration (should fail with AlreadyRegistered)
2382        let v1_already_registered_result = stake_table_state
2383            .clone()
2384            .apply_event(StakeTableEvent::Register((&test_validator).into()));
2385
2386        pretty_assertions::assert_matches!(
2387           v1_already_registered_result,  Err(StakeTableError::AlreadyRegistered(account))
2388                if account == test_validator.account,
2389           "Expected AlreadyRegistered error. version ={version:?} result={v1_already_registered_result:?}",
2390        );
2391
2392        // attempt using V2 registration (should also fail with AlreadyRegistered)
2393        let v2_already_registered_result = stake_table_state
2394            .clone()
2395            .apply_event(StakeTableEvent::RegisterV2((&test_validator).into()));
2396
2397        pretty_assertions::assert_matches!(
2398            v2_already_registered_result,
2399            Err(StakeTableError::AlreadyRegistered(account)) if account == test_validator.account,
2400            "Expected AlreadyRegistered error. version ={version:?} result={v2_already_registered_result:?}",
2401
2402        );
2403
2404        // attempt using V3 registration with a different x25519 key (should also fail
2405        // with AlreadyRegistered because the validator address is already registered)
2406        let v3_already_registered_result =
2407            stake_table_state
2408                .clone()
2409                .apply_event(StakeTableEvent::RegisterV3(
2410                    (&test_validator
2411                        .clone()
2412                        .with_x25519_key([43u8; 32])
2413                        .with_p2p_addr("127.0.0.1:9001"))
2414                        .into(),
2415                ));
2416
2417        pretty_assertions::assert_matches!(
2418            v3_already_registered_result,
2419            Err(StakeTableError::AlreadyRegistered(account)) if account == test_validator.account,
2420            "Expected AlreadyRegistered error. version ={version:?} result={v3_already_registered_result:?}",
2421        );
2422    }
2423
2424    #[test]
2425    fn test_register_validator_v2_auth_fails_marks_as_unauthenticated() {
2426        let mut state = StakeTableState::default();
2427        let mut val = TestValidator::random();
2428        val.bls_sig = Default::default();
2429        let event = StakeTableEvent::RegisterV2((&val).into());
2430
2431        let result = state.apply_event(event);
2432        assert!(
2433            result.is_ok(),
2434            "Validator with invalid auth should still be accepted"
2435        );
2436
2437        let validator = state
2438            .validators()
2439            .get(&val.account)
2440            .expect("validator should exist");
2441        assert!(
2442            !validator.authenticated,
2443            "Validator should be marked as not authenticated"
2444        );
2445
2446        let event = StakeTableEvent::Delegate(Delegated {
2447            delegator: Address::random(),
2448            validator: val.account,
2449            amount: U256::from(100),
2450        });
2451        state.apply_event(event).unwrap().unwrap();
2452
2453        let active = select_active_validator_set(state.validators(), EPOCH_VERSION);
2454        match active {
2455            Err(_) => {}, // No validators is valid - means the unauthenticated one was filtered
2456            Ok(map) => {
2457                assert!(
2458                    map.get(&val.account).is_none(),
2459                    "Unauthenticated validator should not be in active set"
2460                );
2461            },
2462        }
2463    }
2464
2465    #[test]
2466    fn test_register_v2_bad_sig_preserves_parsed_bls_key() {
2467        let val = TestValidator::random();
2468        let other = TestValidator::random();
2469        let bad_val = TestValidator {
2470            bls_sig: other.bls_sig,
2471            ..val.clone()
2472        };
2473
2474        let mut state = StakeTableState::default();
2475        state
2476            .apply_event(StakeTableEvent::RegisterV2((&bad_val).into()))
2477            .expect("no fatal error")
2478            .expect("registered unauthenticated");
2479
2480        let registered = state.validators().get(&val.account).expect("present");
2481        assert!(!registered.authenticated);
2482        let expected_bls = BLSPubKey::try_from(val.bls_vk).expect("valid bls key");
2483        assert_eq!(registered.stake_table_key.as_ref(), Some(&expected_bls));
2484    }
2485
2486    #[test]
2487    fn test_register_v3_bad_sig_preserves_parsed_bls_key() {
2488        let val = TestValidator::random();
2489        let other = TestValidator::random();
2490        let bad_val = TestValidator {
2491            bls_sig: other.bls_sig,
2492            ..val.clone()
2493        };
2494
2495        let mut state = StakeTableState::default();
2496        state
2497            .apply_event(StakeTableEvent::RegisterV3((&bad_val).into()))
2498            .expect("no fatal error")
2499            .expect("registered unauthenticated");
2500
2501        let registered = state.validators().get(&val.account).expect("present");
2502        assert!(!registered.authenticated);
2503        let expected_bls = BLSPubKey::try_from(val.bls_vk).expect("valid bls key");
2504        assert_eq!(registered.stake_table_key.as_ref(), Some(&expected_bls));
2505    }
2506
2507    #[test]
2508    fn test_authenticated_validator_deserialize_rejects_unauthenticated() {
2509        let mut validator = RegisteredValidator::<BLSPubKey>::mock();
2510        validator.authenticated = false;
2511
2512        let json = serde_json::to_string(&validator).unwrap();
2513        let result: Result<AuthenticatedValidator<BLSPubKey>, _> = serde_json::from_str(&json);
2514
2515        assert!(result.is_err());
2516        let err = result.unwrap_err().to_string();
2517        assert!(
2518            err.contains("cannot deserialize unauthenticated validator"),
2519            "unexpected error: {err}"
2520        );
2521    }
2522
2523    #[rstest]
2524    #[case::v1(StakeTableContractVersion::V1)]
2525    #[case::v2(StakeTableContractVersion::V2)]
2526    #[case::v3(StakeTableContractVersion::V3)]
2527    fn test_deregister_validator(#[case] version: StakeTableContractVersion) {
2528        let mut state = StakeTableState::default();
2529        let val = TestValidator::random();
2530
2531        let reg = StakeTableEvent::Register((&val).into());
2532        state.apply_event(reg).unwrap().unwrap();
2533
2534        let dereg = match version {
2535            StakeTableContractVersion::V1 => StakeTableEvent::Deregister((&val).into()),
2536            StakeTableContractVersion::V2 | StakeTableContractVersion::V3 => {
2537                StakeTableEvent::DeregisterV2(ValidatorExitV2 {
2538                    validator: val.account,
2539                    unlocksAt: U256::from(1000u64),
2540                })
2541            },
2542        };
2543        state.apply_event(dereg).unwrap().unwrap();
2544        assert!(!state.validators.contains_key(&val.account));
2545    }
2546
2547    #[rstest]
2548    #[case::v1(StakeTableContractVersion::V1)]
2549    #[case::v2(StakeTableContractVersion::V2)]
2550    #[case::v3(StakeTableContractVersion::V3)]
2551    fn test_delegate_and_undelegate(#[case] version: StakeTableContractVersion) {
2552        let mut state = StakeTableState::default();
2553        let val = TestValidator::random();
2554        state
2555            .apply_event(StakeTableEvent::Register((&val).into()))
2556            .unwrap()
2557            .unwrap();
2558
2559        let delegator = Address::random();
2560        let amount = U256::from(1000);
2561        let delegate_event = StakeTableEvent::Delegate(Delegated {
2562            delegator,
2563            validator: val.account,
2564            amount,
2565        });
2566        state.apply_event(delegate_event).unwrap().unwrap();
2567
2568        let validator = state.validators.get(&val.account).unwrap();
2569        assert_eq!(validator.delegators.get(&delegator).cloned(), Some(amount));
2570
2571        let undelegate_event = match version {
2572            StakeTableContractVersion::V1 => StakeTableEvent::Undelegate(Undelegated {
2573                delegator,
2574                validator: val.account,
2575                amount,
2576            }),
2577            StakeTableContractVersion::V2 | StakeTableContractVersion::V3 => {
2578                StakeTableEvent::UndelegateV2(UndelegatedV2 {
2579                    delegator,
2580                    validator: val.account,
2581                    amount,
2582                    unlocksAt: U256::from(2000u64),
2583                    undelegationId: 1,
2584                })
2585            },
2586        };
2587        state.apply_event(undelegate_event).unwrap().unwrap();
2588        let validator = state.validators.get(&val.account).unwrap();
2589        assert!(!validator.delegators.contains_key(&delegator));
2590    }
2591
2592    #[rstest]
2593    #[case::v1(StakeTableContractVersion::V1)]
2594    #[case::v2(StakeTableContractVersion::V2)]
2595    #[case::v3(StakeTableContractVersion::V3)]
2596    fn test_key_update_event(#[case] version: StakeTableContractVersion) {
2597        let mut state = StakeTableState::default();
2598        let val = TestValidator::random();
2599
2600        // Always register first using V1 to simulate upgrade scenarios
2601        state
2602            .apply_event(StakeTableEvent::Register((&val).into()))
2603            .unwrap()
2604            .unwrap();
2605
2606        let new_keys = val.randomize_keys();
2607
2608        let event = match version {
2609            StakeTableContractVersion::V1 => StakeTableEvent::KeyUpdate((&new_keys).into()),
2610            StakeTableContractVersion::V2 | StakeTableContractVersion::V3 => {
2611                StakeTableEvent::KeyUpdateV2((&new_keys).into())
2612            },
2613        };
2614
2615        state.apply_event(event).unwrap().unwrap();
2616
2617        let updated = state.validators.get(&val.account).unwrap();
2618        let expected_bls = BLSPubKey::try_from(new_keys.bls_vk).expect("valid test BLS key");
2619        assert_eq!(updated.stake_table_key.as_ref(), Some(&expected_bls));
2620        let expected_schnorr =
2621            SchnorrPubKey::try_from(new_keys.schnorr_vk).expect("valid test Schnorr key");
2622        assert_eq!(updated.state_ver_key.as_ref(), Some(&expected_schnorr));
2623    }
2624
2625    #[test]
2626    fn test_duplicate_bls_key() {
2627        let mut state = StakeTableState::default();
2628        let val = TestValidator::random();
2629        let event1 = StakeTableEvent::Register((&val).into());
2630        let mut val2 = TestValidator::random();
2631        val2.bls_vk = val.bls_vk;
2632        val2.account = Address::random();
2633
2634        let event2 = StakeTableEvent::Register((&val2).into());
2635        state.apply_event(event1).unwrap().unwrap();
2636        let result = state.apply_event(event2);
2637
2638        let expected_bls_key = BLSPubKey::try_from(val.bls_vk)
2639            .expect("valid test BLS key")
2640            .to_string();
2641
2642        assert_matches!(
2643            result,
2644            Err(StakeTableError::BlsKeyAlreadyUsed(key))
2645                if key == expected_bls_key,
2646            "Expected BlsKeyAlreadyUsed({expected_bls_key}), but got: {result:?}",
2647        );
2648    }
2649
2650    #[test]
2651    fn test_duplicate_schnorr_key() {
2652        let mut state = StakeTableState::default();
2653        let val = TestValidator::random();
2654        let event1 = StakeTableEvent::Register((&val).into());
2655        let mut val2 = TestValidator::random();
2656        val2.schnorr_vk = val.schnorr_vk;
2657        val2.account = Address::random();
2658        val2.bls_vk = val2.randomize_keys().bls_vk;
2659
2660        let event2 = StakeTableEvent::Register((&val2).into());
2661        state.apply_event(event1).unwrap().unwrap();
2662        let result = state.apply_event(event2);
2663
2664        let schnorr = SchnorrPubKey::try_from(val.schnorr_vk).expect("valid test Schnorr key");
2665        assert_matches!(
2666            result,
2667            Ok(Err(ExpectedStakeTableError::SchnorrKeyAlreadyUsed(key)))
2668                if key == schnorr.to_string(),
2669            "Expected SchnorrKeyAlreadyUsed({schnorr}), but got: {result:?}",
2670
2671        );
2672    }
2673
2674    #[test]
2675    fn test_duplicate_schnorr_key_v2_during_update() {
2676        let mut state = StakeTableState::default();
2677
2678        let val1 = TestValidator::random();
2679
2680        let mut rng = &mut rand::thread_rng();
2681        let bls_key_pair = BLSKeyPair::generate(&mut rng);
2682
2683        let val2 = TestValidator {
2684            bls_vk: bls_key_pair.ver_key().to_affine().into(),
2685            bls_sig: sign_address_bls(&bls_key_pair, val1.account).into(),
2686            ..val1.clone()
2687        };
2688        let event1 = StakeTableEvent::RegisterV2((&val1).into());
2689        let event2 = StakeTableEvent::KeyUpdateV2((&val2).into());
2690
2691        state.apply_event(event1).unwrap().unwrap();
2692        let result = state.apply_event(event2);
2693
2694        let schnorr = SchnorrPubKey::try_from(val1.schnorr_vk).expect("valid test Schnorr key");
2695        assert_matches!(
2696            result,
2697            Err(StakeTableError::SchnorrKeyAlreadyUsed(key))
2698                if key == schnorr.to_string(),
2699            "Expected SchnorrKeyAlreadyUsed({schnorr}), but got: {result:?}",
2700        );
2701    }
2702
2703    #[test]
2704    fn test_register_and_deregister_validator() {
2705        let mut state = StakeTableState::default();
2706        let validator = TestValidator::random();
2707        let event = StakeTableEvent::Register((&validator).into());
2708        state.apply_event(event).unwrap().unwrap();
2709
2710        let deregister_event = StakeTableEvent::Deregister((&validator).into());
2711        assert!(state.apply_event(deregister_event).unwrap().is_ok());
2712    }
2713
2714    #[test]
2715    fn test_commission_validation_exceeds_basis_points() {
2716        // Create a simple stake table with one validator
2717        let validator = TestValidator::random();
2718        let mut stake_table = StakeTableState::default();
2719
2720        // Register the validator first
2721        let registration_event = ValidatorRegistered::from(&validator).into();
2722        stake_table
2723            .apply_event(registration_event)
2724            .unwrap()
2725            .unwrap();
2726
2727        // Test that a commission exactly at the limit is allowed
2728        let valid_commission_event = CommissionUpdated {
2729            validator: validator.account,
2730            timestamp: Default::default(),
2731            oldCommission: 0,
2732            newCommission: COMMISSION_BASIS_POINTS, // Exactly at the limit
2733        }
2734        .into();
2735        stake_table
2736            .apply_event(valid_commission_event)
2737            .unwrap()
2738            .unwrap();
2739
2740        let invalid_commission = COMMISSION_BASIS_POINTS + 1;
2741        let invalid_commission_event = CommissionUpdated {
2742            validator: validator.account,
2743            timestamp: Default::default(),
2744            oldCommission: 0,
2745            newCommission: invalid_commission,
2746        }
2747        .into();
2748
2749        let err = stake_table
2750            .apply_event(invalid_commission_event)
2751            .unwrap_err();
2752
2753        assert_matches!(
2754            err,
2755            StakeTableError::InvalidCommission(addr, invalid_commission)
2756                if addr == addr && invalid_commission == invalid_commission);
2757    }
2758
2759    #[test]
2760    fn test_delegate_zero_amount_is_rejected() {
2761        let mut state = StakeTableState::default();
2762        let validator = TestValidator::random();
2763        let account = validator.account;
2764        state
2765            .apply_event(StakeTableEvent::Register((&validator).into()))
2766            .unwrap()
2767            .unwrap();
2768
2769        let delegator = Address::random();
2770        let amount = U256::ZERO;
2771        let event = StakeTableEvent::Delegate(Delegated {
2772            delegator,
2773            validator: account,
2774            amount,
2775        });
2776        let result = state.apply_event(event);
2777
2778        assert_matches!(
2779            result,
2780            Err(StakeTableError::ZeroDelegatorStake(addr))
2781                if addr == delegator,
2782            "delegator stake is zero"
2783
2784        );
2785    }
2786
2787    #[test]
2788    fn test_undelegate_more_than_stake_fails() {
2789        let mut state = StakeTableState::default();
2790        let validator = TestValidator::random();
2791        let account = validator.account;
2792        state
2793            .apply_event(StakeTableEvent::Register((&validator).into()))
2794            .unwrap()
2795            .unwrap();
2796
2797        let delegator = Address::random();
2798        let event = StakeTableEvent::Delegate(Delegated {
2799            delegator,
2800            validator: account,
2801            amount: U256::from(10u64),
2802        });
2803        state.apply_event(event).unwrap().unwrap();
2804
2805        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
2806            delegator,
2807            validator: account,
2808            amount: U256::from(20u64),
2809        }));
2810        assert_matches!(
2811            result,
2812            Err(StakeTableError::InsufficientStake),
2813            "Expected InsufficientStake error, got: {result:?}",
2814        );
2815    }
2816
2817    #[test]
2818    fn test_apply_event_does_not_modify_state_on_error() {
2819        let mut state = StakeTableState::default();
2820        let validator = TestValidator::random();
2821        let delegator = Address::random();
2822
2823        state
2824            .apply_event(StakeTableEvent::Register((&validator).into()))
2825            .unwrap()
2826            .unwrap();
2827
2828        // AlreadyRegistered error
2829        let state_before = state.clone();
2830        let result = state.apply_event(StakeTableEvent::Register((&validator).into()));
2831        assert_matches!(result, Err(StakeTableError::AlreadyRegistered(_)));
2832        assert_eq!(
2833            state, state_before,
2834            "State should not change on AlreadyRegistered error"
2835        );
2836
2837        // Duplicate BLS key error
2838        let state_before = state.clone();
2839        let mut validator2 = TestValidator::random();
2840        validator2.bls_vk = validator.bls_vk; // Reuse BLS key
2841        let result = state.apply_event(StakeTableEvent::Register((&validator2).into()));
2842        assert_matches!(result, Err(StakeTableError::BlsKeyAlreadyUsed(_)));
2843        assert_eq!(
2844            state, state_before,
2845            "State should not change on BlsKeyAlreadyUsed error"
2846        );
2847
2848        // ValidatorNotFound error on deregister
2849        let state_before = state.clone();
2850        let nonexistent_validator = TestValidator::random();
2851        let result =
2852            state.apply_event(StakeTableEvent::Deregister((&nonexistent_validator).into()));
2853        assert_matches!(result, Err(StakeTableError::ValidatorNotFound(_)));
2854        assert_eq!(
2855            state, state_before,
2856            "State should not change on ValidatorNotFound error"
2857        );
2858
2859        // ValidatorNotFound error on undelegate
2860        let state_before = state.clone();
2861        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
2862            delegator: Address::random(),
2863            validator: Address::random(),
2864            amount: U256::from(100u64),
2865        }));
2866        assert_matches!(result, Err(StakeTableError::ValidatorNotFound(_)));
2867        assert_eq!(
2868            state, state_before,
2869            "State should not change on ValidatorNotFound error for Undelegate"
2870        );
2871
2872        state
2873            .apply_event(StakeTableEvent::Delegate(Delegated {
2874                delegator,
2875                validator: validator.account,
2876                amount: U256::from(100u64),
2877            }))
2878            .unwrap()
2879            .unwrap();
2880
2881        // DelegatorNotFound error on undelegate
2882        let state_before = state.clone();
2883        let non_existent_delegator = Address::random();
2884        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
2885            delegator: non_existent_delegator,
2886            validator: validator.account,
2887            amount: U256::from(50u64),
2888        }));
2889        assert_matches!(result, Err(StakeTableError::DelegatorNotFound(_)));
2890        assert_eq!(
2891            state, state_before,
2892            "State should not change on DelegatorNotFound error"
2893        );
2894
2895        // InsufficientStake error on undelegate
2896        let state_before = state.clone();
2897        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
2898            delegator,
2899            validator: validator.account,
2900            amount: U256::from(200u64),
2901        }));
2902        assert_matches!(result, Err(StakeTableError::InsufficientStake));
2903        assert_eq!(
2904            state, state_before,
2905            "State should not change on InsufficientStake error"
2906        );
2907
2908        // InsufficientStake when validator total stake would be less than amount
2909        let validator2 = TestValidator::random();
2910        let delegator2 = Address::random();
2911
2912        state
2913            .apply_event(StakeTableEvent::Register((&validator2).into()))
2914            .unwrap()
2915            .unwrap();
2916
2917        state
2918            .apply_event(StakeTableEvent::Delegate(Delegated {
2919                delegator: delegator2,
2920                validator: validator2.account,
2921                amount: U256::from(50u64),
2922            }))
2923            .unwrap()
2924            .unwrap();
2925        let state_before = state.clone();
2926        let result = state.apply_event(StakeTableEvent::Undelegate(Undelegated {
2927            delegator: delegator2,
2928            validator: validator2.account,
2929            amount: U256::from(100u64),
2930        }));
2931        assert_matches!(result, Err(StakeTableError::InsufficientStake));
2932        assert_eq!(state, state_before,);
2933
2934        // ZeroDelegatorStake error
2935        let state_before = state.clone();
2936        let result = state.apply_event(StakeTableEvent::Delegate(Delegated {
2937            delegator: Address::random(),
2938            validator: validator.account,
2939            amount: U256::ZERO,
2940        }));
2941        assert_matches!(result, Err(StakeTableError::ZeroDelegatorStake(_)));
2942        assert_eq!(
2943            state, state_before,
2944            "State should not change on ZeroDelegatorStake error"
2945        );
2946    }
2947
2948    #[test_log::test(tokio::test(flavor = "multi_thread"))]
2949    async fn test_decaf_stake_table() {
2950        // The following commented-out block demonstrates how the `decaf_stake_table_events.json`
2951        // and `decaf_stake_table.json` files were originally generated.
2952
2953        // It generates decaf stake table data by fetching events from the contract,
2954        // writes events and the constructed stake table to JSON files.
2955
2956        /*
2957        let l1 = L1Client::new(vec!["https://ethereum-sepolia.publicnode.com"
2958            .parse()
2959            .unwrap()])
2960        .unwrap();
2961        let contract_address = "0x40304fbe94d5e7d1492dd90c53a2d63e8506a037";
2962
2963        let events = Fetcher::fetch_events_from_contract(
2964            l1,
2965            contract_address.parse().unwrap(),
2966            None,
2967            8582328,
2968        )
2969        .await?;
2970
2971        // Serialize and write sorted events
2972        let json_events = serde_json::to_string_pretty(&sorted_events)?;
2973        let mut events_file = File::create("decaf_stake_table_events.json").await?;
2974        events_file.write_all(json_events.as_bytes()).await?;
2975
2976        // Process into stake table
2977        let stake_table = validators_from_l1_events(sorted_events.into_iter().map(|(_, e)| e))?;
2978
2979        // Serialize and write stake table
2980        let json_stake_table = serde_json::to_string_pretty(&stake_table)?;
2981        let mut stake_file = File::create("decaf_stake_table.json").await?;
2982        stake_file.write_all(json_stake_table.as_bytes()).await?;
2983        */
2984
2985        let events_json =
2986            std::fs::read_to_string("../../../data/v3/decaf_stake_table_events.json").unwrap();
2987        let events: Vec<(EventKey, StakeTableEvent)> = serde_json::from_str(&events_json).unwrap();
2988
2989        // Reconstruct stake table from events
2990        let reconstructed_stake_table =
2991            ValidatorSet::from_l1_events(events.into_iter().map(|(_, e)| e), EPOCH_VERSION)
2992                .unwrap()
2993                .active_validators;
2994
2995        let stake_table_json =
2996            std::fs::read_to_string("../../../data/v3/decaf_stake_table.json").unwrap();
2997        let expected: AuthenticatedValidatorMap = serde_json::from_str(&stake_table_json).unwrap();
2998
2999        assert_eq!(
3000            reconstructed_stake_table, expected,
3001            "Stake table reconstructed from events does not match the expected stake table "
3002        );
3003    }
3004
3005    async fn snapshot_stake_table_commit(
3006        network: &str,
3007        rpc_url: &str,
3008        contract: &str,
3009        to_block: u64,
3010    ) {
3011        let l1 = L1ClientOptions {
3012            l1_events_max_retry_duration: Duration::from_secs(120),
3013            l1_events_max_block_range: 10_000,
3014            l1_retry_delay: Duration::from_secs(2),
3015            ..Default::default()
3016        }
3017        .connect(vec![rpc_url.parse().unwrap()])
3018        .expect("unable to construct l1 client");
3019
3020        let events =
3021            Fetcher::fetch_events_from_contract(l1, contract.parse().unwrap(), None, to_block)
3022                .await
3023                .unwrap();
3024
3025        let validator_set =
3026            ValidatorSet::from_l1_events(events.into_iter().map(|(_, e)| e), EPOCH_VERSION)
3027                .expect("failed to build validator set");
3028
3029        let active_as_registered = to_registered_validator_map(validator_set.active_validators());
3030        let active_state = StakeTableState::new(
3031            active_as_registered,
3032            Default::default(),
3033            Default::default(),
3034            Default::default(),
3035            Default::default(),
3036        );
3037        let active_commit = active_state.commit();
3038
3039        let summary = format!(
3040            "network: {network}\nto_block: {to_block}\nstake_table_contract: \
3041             {contract}\nstake_table_hash: {}\nactive_validators_commit: {}\nall_validators: \
3042             {}\nactive_validators: {}\n",
3043            validator_set
3044                .stake_table_hash()
3045                .expect("stake_table_hash should be set"),
3046            active_commit,
3047            validator_set.all_validators().len(),
3048            validator_set.active_validators().len(),
3049        );
3050
3051        let mut settings = insta::Settings::clone_current();
3052        let data_dir = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
3053            .join("../../../data/insta_snapshots");
3054        settings.set_snapshot_path(data_dir);
3055        settings.set_prepend_module_to_snapshot(false);
3056        settings.bind(|| {
3057            insta::assert_snapshot!(format!("{network}_stake_table_snapshot"), summary);
3058        });
3059    }
3060
3061    #[ignore = "talks to public Sepolia RPC"]
3062    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3063    async fn snapshot_decaf_stake_table_commit() {
3064        snapshot_stake_table_commit(
3065            "decaf",
3066            "https://ethereum-sepolia.publicnode.com",
3067            "0x40304fbe94d5e7d1492dd90c53a2d63e8506a037",
3068            10_935_000,
3069        )
3070        .await;
3071    }
3072
3073    #[ignore = "talks to public Ethereum mainnet RPC"]
3074    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3075    async fn snapshot_mainnet_stake_table_commit() {
3076        snapshot_stake_table_commit(
3077            "mainnet",
3078            "https://ethereum-rpc.publicnode.com",
3079            "0xcef474d372b5b09defe2af187bf17338dc704451",
3080            25_188_000,
3081        )
3082        .await;
3083    }
3084
3085    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3086    #[should_panic]
3087    async fn test_large_max_events_range_panic() {
3088        // decaf stake table contract address
3089        let contract_address = "0x40304fbe94d5e7d1492dd90c53a2d63e8506a037";
3090
3091        let l1 = L1ClientOptions {
3092            l1_events_max_retry_duration: Duration::from_secs(30),
3093            // max block range for public node rpc is 50000 so this should result in a panic
3094            l1_events_max_block_range: 10_u64.pow(9),
3095            l1_retry_delay: Duration::from_secs(1),
3096            ..Default::default()
3097        }
3098        .connect(vec![
3099            "https://ethereum-sepolia.publicnode.com".parse().unwrap(),
3100        ])
3101        .expect("unable to construct l1 client");
3102
3103        let latest_block = l1.provider.get_block_number().await.unwrap();
3104        let _events = Fetcher::fetch_events_from_contract(
3105            l1,
3106            contract_address.parse().unwrap(),
3107            None,
3108            latest_block,
3109        )
3110        .await
3111        .unwrap();
3112    }
3113
3114    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3115    async fn sanity_check_block_reward_v3() {
3116        // 10b tokens
3117        let initial_supply = U256::from_str("10000000000000000000000000000").unwrap();
3118
3119        let reward = ((initial_supply * U256::from(INFLATION_RATE)) / U256::from(BLOCKS_PER_YEAR))
3120            .checked_div(U256::from(COMMISSION_BASIS_POINTS))
3121            .unwrap();
3122
3123        println!("Calculated reward: {reward}");
3124        assert!(reward > U256::ZERO);
3125    }
3126
3127    #[test]
3128    fn sanity_check_p_and_rp() {
3129        let total_stake = BigDecimal::from_str("1000").unwrap();
3130        let total_supply = BigDecimal::from_str("10000").unwrap(); // p = 0.1
3131
3132        let (p, rp) =
3133            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3134
3135        assert!(p > BigDecimal::zero());
3136        assert!(p < BigDecimal::one());
3137        assert!(rp > BigDecimal::zero());
3138    }
3139
3140    #[test]
3141    fn test_p_out_of_range() {
3142        let total_stake = BigDecimal::from_str("1000").unwrap();
3143        let total_supply = BigDecimal::from_str("500").unwrap(); // p = 2.0
3144
3145        let result = calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply);
3146        assert!(result.is_err());
3147    }
3148
3149    #[test]
3150    fn test_zero_total_supply() {
3151        let total_stake = BigDecimal::from_str("1000").unwrap();
3152        let total_supply = BigDecimal::from(0);
3153
3154        let result = calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply);
3155        assert!(result.is_err());
3156    }
3157
3158    #[test]
3159    fn test_valid_p_and_rp() {
3160        let total_stake = BigDecimal::from_str("5000").unwrap();
3161        let total_supply = BigDecimal::from_str("10000").unwrap();
3162
3163        let (p, rp) =
3164            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3165
3166        assert_eq!(p, BigDecimal::from_str("0.5").unwrap());
3167        assert!(rp > BigDecimal::from_str("0.0").unwrap());
3168    }
3169
3170    #[test]
3171    fn test_very_small_p() {
3172        let total_stake = BigDecimal::from_str("1").unwrap(); // 1 wei
3173        let total_supply = BigDecimal::from_str("10000000000000000000000000000").unwrap(); // 10B * 1e18
3174
3175        let (p, rp) =
3176            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3177
3178        assert!(p > BigDecimal::from_str("0").unwrap());
3179        assert!(p < BigDecimal::from_str("1e-18").unwrap()); // p should be extremely small
3180        assert!(rp > BigDecimal::zero());
3181    }
3182
3183    #[test]
3184    fn test_p_very_close_to_one() {
3185        let total_stake = BigDecimal::from_str("9999999999999999999999999999").unwrap();
3186        let total_supply = BigDecimal::from_str("10000000000000000000000000000").unwrap();
3187
3188        let (p, rp) =
3189            calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3190
3191        assert!(p < BigDecimal::one());
3192        assert!(p > BigDecimal::from_str("0.999999999999999999999999999").unwrap());
3193        assert!(rp > BigDecimal::zero());
3194    }
3195
3196    /// tests `calculate_proportion_staked_and_reward_rate` produces correct p and R(p) values
3197    /// across a range of stake proportions within a small numerical tolerance.
3198    ///
3199    #[test]
3200    fn test_reward_rate_rp() {
3201        let test_cases = [
3202            // p , R(p)
3203            ("0.0000", "0.2121"), // 0%
3204            ("0.0050", "0.2121"), // 0.5%
3205            ("0.0100", "0.2121"), // 1%
3206            ("0.0250", "0.1342"), // 2.5%
3207            ("0.0500", "0.0949"), // 5%
3208            ("0.1000", "0.0671"), // 10%
3209            ("0.2500", "0.0424"), // 25%
3210            ("0.5000", "0.0300"), // 50%
3211            ("0.7500", "0.0245"), // 75%
3212            ("1.0000", "0.0212"), // 100%
3213        ];
3214
3215        let tolerance = BigDecimal::from_str("0.0001").unwrap();
3216
3217        let total_supply = BigDecimal::from_u32(10_000).unwrap();
3218
3219        for (p, rp) in test_cases {
3220            let p = BigDecimal::from_str(p).unwrap();
3221            let expected_rp = BigDecimal::from_str(rp).unwrap();
3222
3223            let total_stake = &p * &total_supply;
3224
3225            let (computed_p, computed_rp) =
3226                calculate_proportion_staked_and_reward_rate(&total_stake, &total_supply).unwrap();
3227
3228            assert!(
3229                (&computed_p - &p).abs() < tolerance,
3230                "p mismatch: got {computed_p}, expected {p}"
3231            );
3232
3233            assert!(
3234                (&computed_rp - &expected_rp).abs() < tolerance,
3235                "R(p) mismatch for p={p}: got {computed_rp}, expected {expected_rp}"
3236            );
3237        }
3238    }
3239
3240    #[tokio::test(flavor = "multi_thread")]
3241    async fn test_dynamic_block_reward_with_expected_values() {
3242        // 10B tokens = 10_000_000_000 * 10^18
3243        let total_supply = U256::from_str("10000000000000000000000000000").unwrap();
3244        let total_supply_bd = BigDecimal::from_str(&total_supply.to_string()).unwrap();
3245
3246        let test_cases = [
3247            // --- Block time: 1 ms ---
3248            ("0.0000", "0.2121", 1, "0"), // 0% staked → inflation = 0 → 0 tokens
3249            ("0.0050", "0.2121", 1, "3362823439878234"), // 0.105% inflation → ~0.00336 tokens
3250            ("0.0100", "0.2121", 1, "6725646879756468"), // 0.2121% inflation → ~0.00673 tokens
3251            ("0.0250", "0.1342", 1, "10638635210553018"), // 0.3355% inflation → ~0.01064 tokens
3252            ("0.0500", "0.0949", 1, "15046296296296296"), // 0.4745% inflation → ~0.01505 tokens
3253            ("0.1000", "0.0671", 1, "21277270421106037"), // 0.671% inflation → ~0.02128 tokens
3254            ("0.2500", "0.0424", 1, "33612379502790461"), // 1.06% inflation → ~0.03361 tokens
3255            ("0.5000", "0.0300", 1, "47564687975646879"), // 1.5% inflation → ~0.04756 tokens
3256            ("0.7500", "0.0245", 1, "58266742770167427"), // 1.8375% inflation → ~0.05827 tokens
3257            ("1.0000", "0.0212", 1, "67224759005580923"), // 2.12% inflation → ~0.06722 tokens
3258            // --- Block time: 2000 ms (2 seconds) ---
3259            ("0.0000", "0.2121", 2000, "0"), // 0% staked → inflation = 0 → 0 tokens
3260            ("0.0050", "0.2121", 2000, "672564687975646880"), // 0.105% inflation → ~0.67256 tokens
3261            ("0.0100", "0.2121", 2000, "1345129375951293760"), // 0.2121% inflation → ~1.34513 tokens
3262            ("0.0250", "0.1342", 2000, "2127727042110603754"), // 0.3355% inflation → ~2.12773 tokens
3263            ("0.0500", "0.0949", 2000, "3009259259259259259"), // 0.4745% inflation → ~3.00926 tokens
3264            ("0.1000", "0.0671", 2000, "4255454084221207509"), // 0.671% inflation → ~4.25545 tokens
3265            ("0.2500", "0.0424", 2000, "6722475900558092339"), // 1.06% inflation → ~6.72248 tokens
3266            ("0.5000", "0.0300", 2000, "9512937595129375951"), // 1.5% inflation → ~9.51294 tokens
3267            ("0.7500", "0.0245", 2000, "11653348554033485540"), // 1.8375% inflation → ~11.65335 tokens
3268            ("1.0000", "0.0212", 2000, "13444951801116184678"), // 2.12% inflation → ~13.44495 tokens
3269            // --- Block time: 10000 ms (10 seconds) ---
3270            ("0.0000", "0.2121", 10000, "0"), // 0% staked → inflation = 0 → 0 tokens
3271            ("0.0050", "0.2121", 10000, "3362823439878234400"), // 0.105% inflation → ~3.36 tokens
3272            ("0.0100", "0.2121", 10000, "6725646879756468800"), // 0.2121% inflation → ~6.73 tokens
3273            ("0.0250", "0.1342", 10000, "10638635210553018770"), // 0.3355% inflation → ~10.64 tokens
3274            ("0.0500", "0.0949", 10000, "15046296296296296295"), // 0.4745% inflation → ~15.05 tokens
3275            ("0.1000", "0.0671", 10000, "21277270421106037545"), // 0.671% inflation → ~21.28 tokens
3276            ("0.2500", "0.0424", 10000, "33612379502790461695"), // 1.06% inflation → ~33.61 tokens
3277            ("0.5000", "0.0300", 10000, "47564687975646879755"), // 1.5% inflation → ~47.56 tokens
3278            ("0.7500", "0.0245", 10000, "58266742770167427700"), // 1.8375% inflation → ~58.27 tokens
3279            ("1.0000", "0.0212", 10000, "67224759005580923390"), // 2.12% inflation → ~67.22 tokens
3280        ];
3281
3282        let tolerance = U256::from(100_000_000_000_000_000u128); // 0.1 token
3283
3284        for (p, rp, avg_block_time_ms, expected_reward) in test_cases {
3285            let p = BigDecimal::from_str(p).unwrap();
3286            let total_stake_bd = (&p * &total_supply_bd).round(0);
3287            println!("total_stake_bd={total_stake_bd}");
3288
3289            let total_stake = U256::from_str(&total_stake_bd.to_plain_string()).unwrap();
3290            let expected_reward = U256::from_str(expected_reward).unwrap();
3291
3292            let epoch = EpochNumber::new(0);
3293            let actual_reward =
3294                compute_block_reward(epoch, total_supply, total_stake, avg_block_time_ms)
3295                    .unwrap()
3296                    .0;
3297
3298            let diff = if actual_reward > expected_reward {
3299                actual_reward - expected_reward
3300            } else {
3301                expected_reward - actual_reward
3302            };
3303
3304            assert!(
3305                diff <= tolerance,
3306                "Reward mismatch for p = {p}, R(p) = {rp}, block_time = {avg_block_time_ms}: \
3307                 expected = {expected_reward}, actual = {actual_reward}, diff = {diff}"
3308            );
3309        }
3310    }
3311
3312    // Uses V2 events where available. Delegate and CommissionUpdate don't have V2 versions.
3313    #[derive(Debug, Clone, Copy)]
3314    enum EventType {
3315        Delegate,
3316        Undelegate,
3317        KeyUpdate,
3318        CommissionUpdate,
3319        Exit,
3320        X25519KeyUpdate,
3321        P2pAddrUpdate,
3322    }
3323
3324    // Regression for PR #3903: validators with invalid signatures used to be dropped during
3325    // registration, so any later event targeting them failed with ValidatorNotFound and broke
3326    // stake table reconstruction. Now they're stored with authenticated=false and subsequent
3327    // events must succeed against them (though they stay out of the active consensus set).
3328    #[rstest]
3329    #[case::delegate(EventType::Delegate)]
3330    #[case::undelegate(EventType::Undelegate)]
3331    #[case::key_update(EventType::KeyUpdate)]
3332    #[case::commission_update(EventType::CommissionUpdate)]
3333    #[case::exit(EventType::Exit)]
3334    #[case::x25519_key_update(EventType::X25519KeyUpdate)]
3335    #[case::p2p_addr_update(EventType::P2pAddrUpdate)]
3336    fn test_events_targeting_unauthenticated_validator(
3337        #[case] event_type: EventType,
3338    ) -> anyhow::Result<()> {
3339        let mut state = StakeTableState::default();
3340        let mut val = TestValidator::random();
3341        val.bls_sig = Default::default();
3342        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3343
3344        let validator = state.validators().get(&val.account).context("validator")?;
3345        assert!(!validator.authenticated);
3346
3347        let delegator = Address::random();
3348        let initial_amount = U256::from(1000);
3349        state.apply_event(StakeTableEvent::Delegate(Delegated {
3350            delegator,
3351            validator: val.account,
3352            amount: initial_amount,
3353        }))??;
3354
3355        match event_type {
3356            EventType::Delegate => {
3357                let new_delegator = Address::random();
3358                let amount = U256::from(500);
3359                state.apply_event(StakeTableEvent::Delegate(Delegated {
3360                    delegator: new_delegator,
3361                    validator: val.account,
3362                    amount,
3363                }))??;
3364
3365                let validator = state.validators().get(&val.account).context("validator")?;
3366                assert_eq!(validator.stake, initial_amount + amount);
3367                assert_eq!(
3368                    validator.delegators.get(&new_delegator).cloned(),
3369                    Some(amount)
3370                );
3371            },
3372            EventType::Undelegate => {
3373                let undelegate_amount = U256::from(300);
3374                state.apply_event(StakeTableEvent::UndelegateV2(UndelegatedV2 {
3375                    delegator,
3376                    validator: val.account,
3377                    undelegationId: 1,
3378                    amount: undelegate_amount,
3379                    unlocksAt: U256::from(1000u64),
3380                }))??;
3381
3382                let validator = state.validators().get(&val.account).context("validator")?;
3383                assert_eq!(validator.stake, initial_amount - undelegate_amount);
3384                assert_eq!(
3385                    validator.delegators.get(&delegator).cloned(),
3386                    Some(initial_amount - undelegate_amount)
3387                );
3388            },
3389            EventType::KeyUpdate => {
3390                let new_keys = val.randomize_keys();
3391                state.apply_event(StakeTableEvent::KeyUpdateV2((&new_keys).into()))??;
3392
3393                let validator = state.validators().get(&val.account).context("validator")?;
3394                let expected_bls =
3395                    BLSPubKey::try_from(new_keys.bls_vk).expect("valid test BLS key");
3396                let expected_schnorr =
3397                    SchnorrPubKey::try_from(new_keys.schnorr_vk).expect("valid test Schnorr key");
3398                assert_eq!(validator.stake_table_key.as_ref(), Some(&expected_bls));
3399                assert_eq!(validator.state_ver_key.as_ref(), Some(&expected_schnorr));
3400                // KeyUpdateV2 authenticates signatures, so on success the validator
3401                // is promoted to `authenticated=true`, even if a prior registration
3402                // left it unauthenticated.
3403                assert!(validator.authenticated);
3404            },
3405            EventType::CommissionUpdate => {
3406                let new_commission: u16 = 5000;
3407                state.apply_event(StakeTableEvent::CommissionUpdate(CommissionUpdated {
3408                    validator: val.account,
3409                    timestamp: Default::default(),
3410                    oldCommission: val.commission,
3411                    newCommission: new_commission,
3412                }))??;
3413
3414                let validator = state.validators().get(&val.account).context("validator")?;
3415                assert_eq!(validator.commission, new_commission);
3416            },
3417            EventType::Exit => {
3418                state.apply_event(StakeTableEvent::DeregisterV2(ValidatorExitV2 {
3419                    validator: val.account,
3420                    unlocksAt: U256::from(1000u64),
3421                }))??;
3422
3423                assert!(!state.validators().contains_key(&val.account));
3424                return Ok(());
3425            },
3426            EventType::X25519KeyUpdate => {
3427                let new_x25519_key = [99u8; 32];
3428                state.apply_event(val.x25519_update(new_x25519_key))??;
3429
3430                let validator = state.validators().get(&val.account).context("validator")?;
3431                let expected = x25519::PublicKey::try_from(new_x25519_key.as_slice())
3432                    .expect("valid x25519 key");
3433                assert_eq!(validator.x25519_key, Some(expected));
3434                assert!(!validator.authenticated);
3435            },
3436            EventType::P2pAddrUpdate => {
3437                state.apply_event(val.p2p_update("10.0.0.1:9000"))??;
3438
3439                let validator = state.validators().get(&val.account).context("validator")?;
3440                let expected: NetAddr = "10.0.0.1:9000".parse().expect("valid p2p addr");
3441                assert_eq!(validator.p2p_addr, Some(expected));
3442                assert!(!validator.authenticated);
3443            },
3444        }
3445
3446        // For most event types the validator is still unauthenticated and is
3447        // filtered out of the active set. The KeyUpdate case is the exception:
3448        // KeyUpdateV2 verifies signatures and promotes the validator to
3449        // `authenticated=true`, so it becomes eligible for selection.
3450        let active = select_active_validator_set(state.validators(), EPOCH_VERSION);
3451        match event_type {
3452            EventType::KeyUpdate => match active {
3453                Ok(map) => assert!(map.contains_key(&val.account)),
3454                Err(e) => bail!("expected validator to be selected after KeyUpdate, got {e}"),
3455            },
3456            _ => match active {
3457                Err(StakeTableError::NoValidValidators) => {},
3458                Err(e) => bail!("Unexpected error: {e}"),
3459                Ok(map) => assert!(!map.contains_key(&val.account)),
3460            },
3461        }
3462        Ok(())
3463    }
3464
3465    #[test]
3466    fn test_register_v3_sets_x25519_and_p2p() -> anyhow::Result<()> {
3467        let val = TestValidator::random();
3468
3469        let mut state = StakeTableState::default();
3470        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3471
3472        let registered = state.validators().get(&val.account).unwrap();
3473        assert!(registered.authenticated);
3474
3475        let expected_x25519 =
3476            x25519::PublicKey::try_from(val.x25519_key.as_slice()).expect("valid x25519 key");
3477        assert_eq!(registered.x25519_key, Some(expected_x25519));
3478
3479        let expected_p2p: NetAddr = val.p2p_addr.parse().expect("valid p2p addr");
3480        assert_eq!(registered.p2p_addr, Some(expected_p2p));
3481
3482        Ok(())
3483    }
3484
3485    #[test]
3486    fn test_register_v3_invalid_sig() -> anyhow::Result<()> {
3487        let val = TestValidator::random();
3488        let other = TestValidator::random();
3489
3490        // Build a V3 registration with val's keys but other's BLS sig (mismatched)
3491        let bad_val = TestValidator {
3492            bls_sig: other.bls_sig,
3493            ..val.clone()
3494        };
3495        let event = StakeTableEvent::RegisterV3((&bad_val).into());
3496
3497        let mut state = StakeTableState::default();
3498        state.apply_event(event)??;
3499
3500        let registered = state.validators().get(&val.account).unwrap();
3501        assert!(!registered.authenticated);
3502
3503        Ok(())
3504    }
3505
3506    #[test]
3507    fn test_register_v3_hostname_p2p() -> anyhow::Result<()> {
3508        let val = TestValidator::random();
3509
3510        let mut state = StakeTableState::default();
3511        // Empty p2p addr: NetAddr parses it as Name("", 0) which is still Some.
3512        // Non-IP strings become NetAddr::Name variant.
3513        let val = val.with_p2p_addr("my-host:9000");
3514        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3515
3516        let registered = state.validators().get(&val.account).unwrap();
3517        let expected_p2p: NetAddr = "my-host:9000".parse().unwrap();
3518        assert_eq!(registered.p2p_addr, Some(expected_p2p));
3519
3520        Ok(())
3521    }
3522
3523    #[test]
3524    fn test_register_v3_invalid_p2p_addr_degrades_to_none() -> anyhow::Result<()> {
3525        let val = TestValidator::random();
3526
3527        let mut state = StakeTableState::default();
3528        let val = val.with_p2p_addr("host:notaport");
3529        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3530
3531        let registered = state.validators().get(&val.account).unwrap();
3532        assert_eq!(registered.p2p_addr, None);
3533
3534        Ok(())
3535    }
3536
3537    #[test]
3538    fn test_x25519_key_update_sets_value() -> anyhow::Result<()> {
3539        let val = TestValidator::random();
3540
3541        let mut state = StakeTableState::default();
3542        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3543
3544        assert_eq!(
3545            state.validators().get(&val.account).unwrap().x25519_key,
3546            None
3547        );
3548
3549        let x25519_key = [99u8; 32];
3550        state.apply_event(val.x25519_update(x25519_key))??;
3551
3552        let registered = state.validators().get(&val.account).unwrap();
3553        let expected_x25519 =
3554            x25519::PublicKey::try_from(x25519_key.as_slice()).expect("valid x25519 key");
3555        assert_eq!(registered.x25519_key, Some(expected_x25519));
3556
3557        Ok(())
3558    }
3559
3560    #[test]
3561    fn test_p2p_addr_update_sets_value() -> anyhow::Result<()> {
3562        let val = TestValidator::random();
3563
3564        let mut state = StakeTableState::default();
3565        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3566
3567        assert_eq!(state.validators().get(&val.account).unwrap().p2p_addr, None);
3568
3569        let p2p_addr = "10.0.0.1:8080";
3570        state.apply_event(val.p2p_update(p2p_addr))??;
3571
3572        let registered = state.validators().get(&val.account).unwrap();
3573        let expected_p2p: NetAddr = p2p_addr.parse().expect("valid p2p addr");
3574        assert_eq!(registered.p2p_addr, Some(expected_p2p));
3575
3576        Ok(())
3577    }
3578
3579    #[test]
3580    fn test_p2p_addr_unparsable_sets_none() -> anyhow::Result<()> {
3581        let val = TestValidator::random();
3582
3583        let mut state = StakeTableState::default();
3584        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3585
3586        // Set a valid address first.
3587        state.apply_event(val.p2p_update("10.0.0.1:8080"))??;
3588        assert!(
3589            state
3590                .validators()
3591                .get(&val.account)
3592                .unwrap()
3593                .p2p_addr
3594                .is_some()
3595        );
3596
3597        // An address with an invalid port that Rust's NetAddr parser rejects degrades to None.
3598        state.apply_event(val.p2p_update("host:notaport"))??;
3599        assert_eq!(state.validators().get(&val.account).unwrap().p2p_addr, None);
3600
3601        Ok(())
3602    }
3603
3604    #[test]
3605    fn test_x25519_key_update_unknown_validator() {
3606        let mut state = StakeTableState::default();
3607        let unknown = TestValidator::random();
3608
3609        let result = state.apply_event(unknown.x25519_update([1u8; 32]));
3610        assert_matches!(
3611            result,
3612            Err(StakeTableError::ValidatorNotFound(addr)) if addr == unknown.account
3613        );
3614    }
3615
3616    #[test]
3617    fn test_p2p_addr_update_unknown_validator() {
3618        let mut state = StakeTableState::default();
3619        let unknown = TestValidator::random();
3620
3621        let result = state.apply_event(unknown.p2p_update("127.0.0.1:9000"));
3622        assert_matches!(
3623            result,
3624            Err(StakeTableError::ValidatorNotFound(addr)) if addr == unknown.account
3625        );
3626    }
3627
3628    #[test]
3629    fn test_x25519_key_update_duplicate() -> anyhow::Result<()> {
3630        let shared_key = [55u8; 32];
3631        let val1 = TestValidator::random().with_x25519_key(shared_key);
3632        let val2 = TestValidator::random().with_x25519_key([2u8; 32]);
3633
3634        let mut state = StakeTableState::default();
3635        // Register both validators via V3
3636        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
3637        state.apply_event(StakeTableEvent::RegisterV3((&val2).into()))??;
3638
3639        // Try to update val2's x25519 key to the same as val1's
3640        let result = state.apply_event(val2.x25519_update(shared_key));
3641        assert_matches!(result, Err(StakeTableError::X25519KeyAlreadyUsed(_)));
3642
3643        Ok(())
3644    }
3645
3646    #[test]
3647    fn test_p2p_addr_update_hostname() -> anyhow::Result<()> {
3648        let val = TestValidator::random();
3649
3650        let mut state = StakeTableState::default();
3651        state.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3652
3653        state.apply_event(val.p2p_update("my-node.example.com:9000"))??;
3654
3655        let registered = state.validators().get(&val.account).unwrap();
3656        let expected_p2p: NetAddr = "my-node.example.com:9000".parse().unwrap();
3657        assert_eq!(registered.p2p_addr, Some(expected_p2p));
3658
3659        Ok(())
3660    }
3661
3662    #[test]
3663    fn test_register_v3_duplicate_bls_key() -> anyhow::Result<()> {
3664        let val1 = TestValidator::random();
3665        let mut val2 = TestValidator::random();
3666        val2.bls_vk = val1.bls_vk;
3667
3668        let mut state = StakeTableState::default();
3669        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
3670        let result = state.apply_event(StakeTableEvent::RegisterV3((&val2).into()));
3671        assert_matches!(result, Err(StakeTableError::BlsKeyAlreadyUsed(_)));
3672
3673        Ok(())
3674    }
3675
3676    #[test]
3677    fn test_register_v3_duplicate_schnorr_key() -> anyhow::Result<()> {
3678        let val1 = TestValidator::random();
3679        let mut val2 = TestValidator::random();
3680        val2.schnorr_vk = val1.schnorr_vk;
3681
3682        let mut state = StakeTableState::default();
3683        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
3684        let result = state.apply_event(StakeTableEvent::RegisterV3((&val2).into()));
3685        assert_matches!(result, Err(StakeTableError::SchnorrKeyAlreadyUsed(_)));
3686
3687        Ok(())
3688    }
3689
3690    #[test]
3691    fn test_register_v3_duplicate_x25519_key() -> anyhow::Result<()> {
3692        let shared_x25519 = [7u8; 32];
3693        let val1 = TestValidator::random().with_x25519_key(shared_x25519);
3694        let val2 = TestValidator::random().with_x25519_key(shared_x25519);
3695
3696        let mut state = StakeTableState::default();
3697        state.apply_event(StakeTableEvent::RegisterV3((&val1).into()))??;
3698        let result = state.apply_event(StakeTableEvent::RegisterV3((&val2).into()));
3699        assert_matches!(result, Err(StakeTableError::X25519KeyAlreadyUsed(_)));
3700
3701        Ok(())
3702    }
3703
3704    #[test]
3705    fn test_register_v3_rejects_exited_validator() -> anyhow::Result<()> {
3706        let val = TestValidator::random();
3707
3708        let mut state = StakeTableState::default();
3709        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3710        state.apply_event(StakeTableEvent::DeregisterV2(ValidatorExitV2 {
3711            validator: val.account,
3712            unlocksAt: U256::from(1000u64),
3713        }))??;
3714
3715        let result = state.apply_event(StakeTableEvent::RegisterV3((&val).into()));
3716        assert_matches!(
3717            result,
3718            Err(StakeTableError::ValidatorAlreadyExited(addr)) if addr == val.account
3719        );
3720
3721        Ok(())
3722    }
3723
3724    #[test]
3725    fn test_register_v3_zero_x25519_soft_fails() -> anyhow::Result<()> {
3726        let val = TestValidator::random().with_x25519_key([0u8; 32]);
3727
3728        let mut state = StakeTableState::default();
3729        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3730
3731        let registered = state.validators().get(&val.account).unwrap();
3732        assert!(registered.x25519_key.is_none());
3733        assert!(state.used_x25519_keys().is_empty());
3734
3735        Ok(())
3736    }
3737
3738    #[test]
3739    fn test_x25519_key_update_zero_soft_fails() -> anyhow::Result<()> {
3740        let val = TestValidator::random();
3741
3742        let mut state = StakeTableState::default();
3743        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3744
3745        state.apply_event(val.x25519_update([0u8; 32]))??;
3746        let registered = state.validators().get(&val.account).unwrap();
3747        assert!(registered.x25519_key.is_none());
3748        assert_eq!(state.used_x25519_keys().len(), 1);
3749
3750        Ok(())
3751    }
3752
3753    /// LE encoding of the curve25519 field prime 2^255 - 19: nonzero but
3754    /// rejected by the Rust parser as non-canonical.
3755    fn noncanonical_x25519_key() -> [u8; 32] {
3756        let mut key = [0xffu8; 32];
3757        key[0] = 0xed;
3758        key[31] = 0x7f;
3759        key
3760    }
3761
3762    /// An invalid x25519 key must not halt event processing;
3763    /// the validator registers with `x25519_key: None`.
3764    #[test]
3765    fn test_register_v3_noncanonical_x25519_soft_fails() -> anyhow::Result<()> {
3766        let val = TestValidator::random().with_x25519_key(noncanonical_x25519_key());
3767
3768        let mut state = StakeTableState::default();
3769        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3770
3771        let registered = state.validators().get(&val.account).unwrap();
3772        assert!(registered.x25519_key.is_none());
3773        assert!(state.used_x25519_keys().is_empty());
3774
3775        Ok(())
3776    }
3777
3778    /// An invalid x25519 key in an update must not halt event
3779    /// processing; the validator's key is cleared.
3780    #[test]
3781    fn test_x25519_key_update_noncanonical_soft_fails() -> anyhow::Result<()> {
3782        let val = TestValidator::random();
3783
3784        let mut state = StakeTableState::default();
3785        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3786        assert!(
3787            state
3788                .validators()
3789                .get(&val.account)
3790                .unwrap()
3791                .x25519_key
3792                .is_some()
3793        );
3794
3795        state.apply_event(val.x25519_update(noncanonical_x25519_key()))??;
3796
3797        let registered = state.validators().get(&val.account).unwrap();
3798        assert!(registered.x25519_key.is_none());
3799        assert_eq!(state.used_x25519_keys().len(), 1);
3800
3801        state.apply_event(val.x25519_update([42u8; 32]))??;
3802        let registered = state.validators().get(&val.account).unwrap();
3803        assert!(registered.x25519_key.is_some());
3804        assert_eq!(state.used_x25519_keys().len(), 2);
3805
3806        Ok(())
3807    }
3808
3809    /// `used_x25519_keys` must be part of the state commitment. Without this,
3810    /// nodes diverging on x25519 uniqueness tracking would produce identical
3811    /// state hashes and the divergence would go undetected.
3812    #[test]
3813    fn test_state_commit_includes_used_x25519_keys() -> anyhow::Result<()> {
3814        let val = TestValidator::random();
3815
3816        let mut state_without = StakeTableState::default();
3817        state_without.apply_event(StakeTableEvent::RegisterV2((&val).into()))??;
3818
3819        let mut state_with = state_without.clone();
3820        state_with
3821            .used_x25519_keys
3822            .insert(x25519::PublicKey::try_from([7u8; 32].as_slice()).unwrap());
3823
3824        assert_ne!(state_without.commit(), state_with.commit());
3825        Ok(())
3826    }
3827
3828    // --- NEW_PROTOCOL_VERSION selection filter tests ---
3829
3830    /// Construct a `RegisteredValidator` with both x25519_key and p2p_addr populated.
3831    fn complete_mock_validator() -> RegisteredValidator<BLSPubKey> {
3832        let mut v = RegisteredValidator::<BLSPubKey>::mock();
3833        v.x25519_key = Some(x25519::PublicKey::try_from([42u8; 32].as_slice()).unwrap());
3834        v.p2p_addr = Some("127.0.0.1:9000".parse().unwrap());
3835        v
3836    }
3837
3838    /// Pre-upgrade: validators with missing x25519/p2p fields must still be selected.
3839    #[test]
3840    fn test_select_pre_upgrade_includes_validators_missing_network_info() {
3841        let mut map = RegisteredValidatorMap::new();
3842
3843        let incomplete = RegisteredValidator::<BLSPubKey>::mock();
3844        assert!(incomplete.x25519_key.is_none());
3845        assert!(incomplete.p2p_addr.is_none());
3846        map.insert(incomplete.account, incomplete.clone());
3847
3848        let complete = complete_mock_validator();
3849        map.insert(complete.account, complete.clone());
3850
3851        let active = select_active_validator_set(&map, EPOCH_VERSION).unwrap();
3852        assert!(active.contains_key(&incomplete.account));
3853        assert!(active.contains_key(&complete.account));
3854    }
3855
3856    /// Post-upgrade: validator missing x25519_key is filtered out.
3857    #[test]
3858    fn test_select_post_upgrade_excludes_missing_x25519() {
3859        let mut map = RegisteredValidatorMap::new();
3860
3861        let mut missing_x25519 = complete_mock_validator();
3862        missing_x25519.x25519_key = None;
3863        map.insert(missing_x25519.account, missing_x25519.clone());
3864
3865        let complete = complete_mock_validator();
3866        map.insert(complete.account, complete.clone());
3867
3868        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
3869        assert!(!active.contains_key(&missing_x25519.account));
3870        assert!(active.contains_key(&complete.account));
3871    }
3872
3873    /// Post-upgrade: validator missing p2p_addr is filtered out.
3874    #[test]
3875    fn test_select_post_upgrade_excludes_missing_p2p() {
3876        let mut map = RegisteredValidatorMap::new();
3877
3878        let mut missing_p2p = complete_mock_validator();
3879        missing_p2p.p2p_addr = None;
3880        map.insert(missing_p2p.account, missing_p2p.clone());
3881
3882        let complete = complete_mock_validator();
3883        map.insert(complete.account, complete.clone());
3884
3885        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
3886        assert!(!active.contains_key(&missing_p2p.account));
3887        assert!(active.contains_key(&complete.account));
3888    }
3889
3890    /// Post-upgrade: a single complete validator is kept.
3891    #[test]
3892    fn test_select_post_upgrade_keeps_complete() {
3893        let mut map = RegisteredValidatorMap::new();
3894        let complete = complete_mock_validator();
3895        map.insert(complete.account, complete.clone());
3896
3897        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
3898        assert!(active.contains_key(&complete.account));
3899    }
3900
3901    /// Post-upgrade: if all validators are incomplete, selection returns
3902    /// `NoValidValidators`.
3903    #[test]
3904    fn test_select_post_upgrade_all_incomplete_fails() {
3905        let mut map = RegisteredValidatorMap::new();
3906        for _ in 0..5 {
3907            let v = RegisteredValidator::<BLSPubKey>::mock();
3908            map.insert(v.account, v);
3909        }
3910
3911        let result = select_active_validator_set(&map, NEW_PROTOCOL_VERSION);
3912        assert_matches!(result, Err(StakeTableError::NoValidValidators));
3913    }
3914
3915    /// Post-upgrade: when more than MAX_VALIDATORS complete validators exist alongside
3916    /// incomplete ones, only complete validators appear in the result and the count is
3917    /// clamped to MAX_VALIDATORS.
3918    #[test]
3919    fn test_select_post_upgrade_top_n_only_complete() {
3920        let mut map = RegisteredValidatorMap::new();
3921        let mut complete_accounts = HashSet::new();
3922        let mut incomplete_accounts = HashSet::new();
3923
3924        // Build MAX_VALIDATORS + 50 validators, alternating complete/incomplete. Give
3925        // complete validators higher stake than incomplete ones so both the filter and
3926        // the stake-based top-N truncation cleanly pick complete ones.
3927        for i in 0..(MAX_VALIDATORS + 50) {
3928            if i % 2 == 0 {
3929                let mut v = complete_mock_validator();
3930                v.stake = U256::from(1_000_000_000_u64 + i as u64);
3931                complete_accounts.insert(v.account);
3932                map.insert(v.account, v);
3933            } else {
3934                let mut v = RegisteredValidator::<BLSPubKey>::mock();
3935                v.stake = U256::from(10_u64);
3936                incomplete_accounts.insert(v.account);
3937                map.insert(v.account, v);
3938            }
3939        }
3940
3941        let active = select_active_validator_set(&map, NEW_PROTOCOL_VERSION).unwrap();
3942
3943        assert!(
3944            active.len() <= MAX_VALIDATORS,
3945            "active has {} validators, expected at most {}",
3946            active.len(),
3947            MAX_VALIDATORS
3948        );
3949        for addr in active.keys() {
3950            assert!(
3951                complete_accounts.contains(addr),
3952                "incomplete validator {addr:?} ended up in active set"
3953            );
3954            assert!(!incomplete_accounts.contains(addr));
3955        }
3956    }
3957
3958    /// Verify the version boundary: the filter only kicks in at/after
3959    /// `NEW_PROTOCOL_VERSION`. A validator missing the x25519 key must be retained at
3960    /// every prior version and rejected at `NEW_PROTOCOL_VERSION`.
3961    #[test]
3962    fn test_select_version_boundary() {
3963        let mut v = complete_mock_validator();
3964        v.x25519_key = None;
3965        let mut map = RegisteredValidatorMap::new();
3966        map.insert(v.account, v.clone());
3967
3968        for protocol_version in [
3969            EPOCH_VERSION,
3970            DRB_AND_HEADER_UPGRADE_VERSION,
3971            EPOCH_REWARD_VERSION,
3972        ] {
3973            let active = select_active_validator_set(&map, protocol_version).unwrap();
3974            assert!(
3975                active.contains_key(&v.account),
3976                "missing x25519 validator should be included at protocol version \
3977                 {protocol_version}",
3978            );
3979        }
3980
3981        let result = select_active_validator_set(&map, NEW_PROTOCOL_VERSION);
3982        assert_matches!(result, Err(StakeTableError::NoValidValidators));
3983    }
3984
3985    /// Integration test: a RegisterV3 event with an unparsable p2p address results in
3986    /// `p2p_addr: None`, which is accepted pre-upgrade and rejected post-upgrade.
3987    #[test]
3988    fn test_p2p_parse_fail_pre_upgrade_included_post_upgrade_excluded() -> anyhow::Result<()> {
3989        let val = TestValidator::random().with_p2p_addr("host:notaport");
3990
3991        let mut state = StakeTableState::default();
3992        state.apply_event(StakeTableEvent::RegisterV3((&val).into()))??;
3993        // Delegate so the validator passes the stake/delegator checks.
3994        state.apply_event(StakeTableEvent::Delegate(Delegated {
3995            delegator: Address::random(),
3996            validator: val.account,
3997            amount: U256::from(100),
3998        }))??;
3999
4000        let registered = state.validators().get(&val.account).unwrap();
4001        assert_eq!(registered.p2p_addr, None);
4002
4003        // Pre-upgrade: the validator is selected.
4004        let pre = select_active_validator_set(state.validators(), EPOCH_VERSION).unwrap();
4005        assert!(pre.contains_key(&val.account));
4006
4007        // Post-upgrade: the sole validator is filtered out, so selection fails.
4008        let post = select_active_validator_set(state.validators(), NEW_PROTOCOL_VERSION);
4009        assert_matches!(post, Err(StakeTableError::NoValidValidators));
4010
4011        Ok(())
4012    }
4013
4014    #[test]
4015    fn test_register_v1_zero_g2_excluded_from_active_set() {
4016        let account = Address::random();
4017        let mut state = StakeTableState::default();
4018        state
4019            .apply_event(StakeTableEvent::Register(ValidatorRegistered {
4020                account,
4021                blsVk: zero_g2(),
4022                schnorrVk: StateKeyPair::generate().ver_key().into(),
4023                commission: 0,
4024            }))
4025            .unwrap()
4026            .unwrap();
4027
4028        let v = state.validators().get(&account).expect("present");
4029        assert!(v.stake_table_key.is_none());
4030        assert!(!v.authenticated);
4031
4032        state
4033            .apply_event(StakeTableEvent::Delegate(Delegated {
4034                delegator: Address::random(),
4035                validator: account,
4036                amount: U256::from(100),
4037            }))
4038            .unwrap()
4039            .unwrap();
4040
4041        match select_active_validator_set(state.validators(), EPOCH_VERSION) {
4042            Err(StakeTableError::NoValidValidators) => {},
4043            Ok(map) => assert!(map.get(&account).is_none()),
4044            Err(e) => panic!("unexpected error: {e}"),
4045        }
4046    }
4047
4048    #[test]
4049    fn test_register_v2_zero_g2_excluded_from_active_set() {
4050        let account = Address::random();
4051        let mut state = StakeTableState::default();
4052        state
4053            .apply_event(StakeTableEvent::RegisterV2(ValidatorRegisteredV2 {
4054                account,
4055                blsVK: zero_g2(),
4056                schnorrVK: StateKeyPair::generate().ver_key().into(),
4057                commission: 0,
4058                blsSig: zero_g1().into(),
4059                schnorrSig: Bytes::default(),
4060                metadataUri: String::new(),
4061            }))
4062            .unwrap()
4063            .unwrap();
4064
4065        let v = state.validators().get(&account).expect("present");
4066        assert!(v.stake_table_key.is_none());
4067        assert!(!v.authenticated);
4068
4069        state
4070            .apply_event(StakeTableEvent::Delegate(Delegated {
4071                delegator: Address::random(),
4072                validator: account,
4073                amount: U256::from(100),
4074            }))
4075            .unwrap()
4076            .unwrap();
4077
4078        match select_active_validator_set(state.validators(), EPOCH_VERSION) {
4079            Err(StakeTableError::NoValidValidators) => {},
4080            Ok(map) => assert!(map.get(&account).is_none()),
4081            Err(e) => panic!("unexpected error: {e}"),
4082        }
4083    }
4084
4085    #[test]
4086    fn test_register_v3_zero_g2_excluded_from_active_set() {
4087        let val = TestValidator::random();
4088        let account = val.account;
4089        let mut state = StakeTableState::default();
4090        state
4091            .apply_event(StakeTableEvent::RegisterV3(register_v3_with_bls(
4092                &val,
4093                zero_g2(),
4094            )))
4095            .unwrap()
4096            .unwrap();
4097
4098        let v = state.validators().get(&account).expect("present");
4099        assert!(v.stake_table_key.is_none());
4100        assert!(!v.authenticated);
4101
4102        state
4103            .apply_event(StakeTableEvent::Delegate(Delegated {
4104                delegator: Address::random(),
4105                validator: account,
4106                amount: U256::from(100),
4107            }))
4108            .unwrap()
4109            .unwrap();
4110
4111        assert_excluded_from_active_set(&state, account);
4112    }
4113
4114    fn zero_schnorr() -> hotshot_contract_adapter::sol_types::EdOnBN254PointSol {
4115        hotshot_contract_adapter::sol_types::EdOnBN254PointSol {
4116            x: U256::ZERO,
4117            y: U256::ZERO,
4118        }
4119    }
4120
4121    /// The twisted Edwards identity (0, 1): on-curve and in-subgroup but not a
4122    /// valid key.
4123    fn identity_schnorr() -> hotshot_contract_adapter::sol_types::EdOnBN254PointSol {
4124        hotshot_contract_adapter::sol_types::EdOnBN254PointSol {
4125            x: U256::ZERO,
4126            y: U256::from(1),
4127        }
4128    }
4129
4130    /// Build a valid V3 registration for `val` but overwrite `blsVK` with a poison value.
4131    /// The blsSig is for the original key so `authenticate()` will also fail, which is
4132    /// fine: the BLS key is unparsable regardless.
4133    fn register_v3_with_bls(val: &TestValidator, bls: G2PointSol) -> ValidatorRegisteredV3 {
4134        let mut reg = ValidatorRegisteredV3::from(val);
4135        reg.blsVK = bls;
4136        reg
4137    }
4138
4139    /// Check that `account` is excluded from the active validator set.
4140    fn assert_excluded_from_active_set(state: &StakeTableState, account: Address) {
4141        match select_active_validator_set(state.validators(), EPOCH_VERSION) {
4142            Err(StakeTableError::NoValidValidators) => {},
4143            Ok(map) => assert!(map.get(&account).is_none()),
4144            Err(e) => panic!("unexpected error: {e}"),
4145        }
4146    }
4147
4148    #[test]
4149    fn test_register_v1_identity_schnorr_excluded_from_active_set() {
4150        let account = Address::random();
4151        let bls: G2PointSol = BLSPubKey::generated_from_seed_indexed([8u8; 32], 0)
4152            .0
4153            .into();
4154        let mut state = StakeTableState::default();
4155        state
4156            .apply_event(StakeTableEvent::Register(ValidatorRegistered {
4157                account,
4158                blsVk: bls,
4159                schnorrVk: identity_schnorr(),
4160                commission: 0,
4161            }))
4162            .unwrap()
4163            .unwrap();
4164
4165        let v = state.validators().get(&account).expect("present");
4166        assert!(
4167            !v.authenticated,
4168            "validator with identity schnorr key must be unauthenticated"
4169        );
4170        assert!(v.state_ver_key.is_none());
4171
4172        state
4173            .apply_event(StakeTableEvent::Delegate(Delegated {
4174                delegator: Address::random(),
4175                validator: account,
4176                amount: U256::from(100),
4177            }))
4178            .unwrap()
4179            .unwrap();
4180
4181        match select_active_validator_set(state.validators(), EPOCH_VERSION) {
4182            Err(StakeTableError::NoValidValidators) => {},
4183            Ok(map) => assert!(map.get(&account).is_none()),
4184            Err(e) => panic!("unexpected error: {e}"),
4185        }
4186    }
4187
4188    #[test]
4189    fn test_register_v1_invalid_schnorr_excluded_from_active_set() {
4190        let account = Address::random();
4191        let bls: G2PointSol = BLSPubKey::generated_from_seed_indexed([9u8; 32], 0)
4192            .0
4193            .into();
4194        let mut state = StakeTableState::default();
4195        state
4196            .apply_event(StakeTableEvent::Register(ValidatorRegistered {
4197                account,
4198                blsVk: bls,
4199                schnorrVk: zero_schnorr(),
4200                commission: 0,
4201            }))
4202            .unwrap()
4203            .unwrap();
4204
4205        let v = state.validators().get(&account).expect("present");
4206        assert!(
4207            !v.authenticated,
4208            "validator with zero schnorr key must be unauthenticated"
4209        );
4210
4211        state
4212            .apply_event(StakeTableEvent::Delegate(Delegated {
4213                delegator: Address::random(),
4214                validator: account,
4215                amount: U256::from(100),
4216            }))
4217            .unwrap()
4218            .unwrap();
4219
4220        match select_active_validator_set(state.validators(), EPOCH_VERSION) {
4221            Err(StakeTableError::NoValidValidators) => {},
4222            Ok(map) => assert!(map.get(&account).is_none()),
4223            Err(e) => panic!("unexpected error: {e}"),
4224        }
4225    }
4226
4227    #[test]
4228    fn test_register_v3_identity_schnorr_excluded_from_active_set() {
4229        let val = TestValidator::random();
4230        let account = val.account;
4231        let mut reg = ValidatorRegisteredV3::from(&val);
4232        reg.schnorrVK = identity_schnorr();
4233        let mut state = StakeTableState::default();
4234        state
4235            .apply_event(StakeTableEvent::RegisterV3(reg))
4236            .unwrap()
4237            .unwrap();
4238
4239        let v = state.validators().get(&account).expect("present");
4240        assert!(
4241            !v.authenticated,
4242            "validator with identity schnorr key must be unauthenticated"
4243        );
4244        assert!(v.state_ver_key.is_none());
4245
4246        state
4247            .apply_event(StakeTableEvent::Delegate(Delegated {
4248                delegator: Address::random(),
4249                validator: account,
4250                amount: U256::from(100),
4251            }))
4252            .unwrap()
4253            .unwrap();
4254
4255        assert_excluded_from_active_set(&state, account);
4256    }
4257
4258    #[test]
4259    fn test_register_v3_invalid_schnorr_excluded_from_active_set() {
4260        let val = TestValidator::random();
4261        let account = val.account;
4262        let mut reg = ValidatorRegisteredV3::from(&val);
4263        reg.schnorrVK = zero_schnorr();
4264        let mut state = StakeTableState::default();
4265        state
4266            .apply_event(StakeTableEvent::RegisterV3(reg))
4267            .unwrap()
4268            .unwrap();
4269
4270        let v = state.validators().get(&account).expect("present");
4271        assert!(
4272            !v.authenticated,
4273            "validator with zero schnorr key must be unauthenticated"
4274        );
4275        assert!(v.state_ver_key.is_none());
4276
4277        state
4278            .apply_event(StakeTableEvent::Delegate(Delegated {
4279                delegator: Address::random(),
4280                validator: account,
4281                amount: U256::from(100),
4282            }))
4283            .unwrap()
4284            .unwrap();
4285
4286        assert_excluded_from_active_set(&state, account);
4287    }
4288
4289    #[test]
4290    fn test_zero_bls_key_register_unauthenticated() {
4291        let account = Address::random();
4292        let schnorr_key_pair = StateKeyPair::generate();
4293        let mut state = StakeTableState::default();
4294
4295        let reg_v2 = ValidatorRegisteredV2 {
4296            account,
4297            blsVK: zero_g2(),
4298            schnorrVK: schnorr_key_pair.ver_key().into(),
4299            commission: 0,
4300            blsSig: zero_g1().into(),
4301            schnorrSig: Bytes::default(),
4302            metadataUri: String::new(),
4303        };
4304        state
4305            .apply_event(StakeTableEvent::RegisterV2(reg_v2))
4306            .expect("no fatal error")
4307            .expect("registered as unauthenticated");
4308        let registered = state.validators().get(&account).expect("present");
4309        assert!(!registered.authenticated);
4310        assert!(registered.stake_table_key.is_none());
4311        assert!(AuthenticatedValidator::try_from(registered).is_err());
4312
4313        // V1 Register on a fresh state takes the same path.
4314        let account_v1 = Address::random();
4315        let schnorr_v1 = StateKeyPair::generate();
4316        let mut state_v1 = StakeTableState::default();
4317        let reg_v1 = ValidatorRegistered {
4318            account: account_v1,
4319            blsVk: zero_g2(),
4320            schnorrVk: schnorr_v1.ver_key().into(),
4321            commission: 0,
4322        };
4323        state_v1
4324            .apply_event(StakeTableEvent::Register(reg_v1))
4325            .expect("no fatal error")
4326            .expect("registered as unauthenticated");
4327        let registered_v1 = state_v1.validators().get(&account_v1).expect("present");
4328        assert!(!registered_v1.authenticated);
4329        assert!(registered_v1.stake_table_key.is_none());
4330
4331        // KeyUpdateV2 with unparsable key: soft skip, prior key preserved.
4332        let valid_val = TestValidator::random();
4333        state
4334            .apply_event(StakeTableEvent::RegisterV2((&valid_val).into()))
4335            .unwrap()
4336            .unwrap();
4337        let prior_key_v2 = state
4338            .validators()
4339            .get(&valid_val.account)
4340            .unwrap()
4341            .stake_table_key;
4342        let update = ConsensusKeysUpdatedV2 {
4343            account: valid_val.account,
4344            blsVK: zero_g2(),
4345            schnorrVK: schnorr_key_pair.ver_key().into(),
4346            blsSig: zero_g1().into(),
4347            schnorrSig: Bytes::default(),
4348        };
4349        assert_matches!(
4350            state.apply_event(StakeTableEvent::KeyUpdateV2(update)),
4351            Ok(Err(ExpectedStakeTableError::InvalidBlsKey))
4352        );
4353        assert_eq!(
4354            state
4355                .validators()
4356                .get(&valid_val.account)
4357                .unwrap()
4358                .stake_table_key,
4359            prior_key_v2
4360        );
4361
4362        // V1 KeyUpdate keeps the skip-event behavior so the prior valid key is preserved.
4363        let valid_v1 = TestValidator::random();
4364        state_v1
4365            .apply_event(StakeTableEvent::Register((&valid_v1).into()))
4366            .unwrap()
4367            .unwrap();
4368        let prior_key = state_v1
4369            .validators()
4370            .get(&valid_v1.account)
4371            .unwrap()
4372            .stake_table_key;
4373        let update_v1 = ConsensusKeysUpdated {
4374            account: valid_v1.account,
4375            blsVK: zero_g2(),
4376            schnorrVK: schnorr_key_pair.ver_key().into(),
4377        };
4378        assert_matches!(
4379            state_v1.apply_event(StakeTableEvent::KeyUpdate(update_v1)),
4380            Ok(Err(ExpectedStakeTableError::InvalidBlsKey))
4381        );
4382        assert_eq!(
4383            state_v1
4384                .validators()
4385                .get(&valid_v1.account)
4386                .unwrap()
4387                .stake_table_key,
4388            prior_key
4389        );
4390    }
4391
4392    /// Regression for the bug: a Register with an unparsable BLS key must
4393    /// leave the validator in the map so subsequent events to that account
4394    /// (delegations, exits) apply cleanly instead of hitting ValidatorNotFound.
4395    #[test]
4396    fn test_zero_bls_key_registered_unauthenticated_in_from_l1_events() {
4397        let valid_val = TestValidator::random();
4398        let poison_account = Address::random();
4399        let schnorr_key_pair = StateKeyPair::generate();
4400
4401        let events = vec![
4402            StakeTableEvent::RegisterV2((&valid_val).into()),
4403            StakeTableEvent::RegisterV2(ValidatorRegisteredV2 {
4404                account: poison_account,
4405                blsVK: zero_g2(),
4406                schnorrVK: schnorr_key_pair.ver_key().into(),
4407                commission: 0,
4408                blsSig: zero_g1().into(),
4409                schnorrSig: Bytes::default(),
4410                metadataUri: String::new(),
4411            }),
4412            StakeTableEvent::Delegate(Delegated {
4413                delegator: Address::random(),
4414                validator: poison_account,
4415                amount: U256::from(10),
4416            }),
4417        ];
4418
4419        let (validators, _) = validators_from_l1_events(events.into_iter()).expect("must not fail");
4420        let valid = validators
4421            .get(&valid_val.account)
4422            .expect("valid validator present");
4423        assert!(valid.authenticated);
4424
4425        let poisoned = validators
4426            .get(&poison_account)
4427            .expect("poison validator present as unauthenticated");
4428        assert!(!poisoned.authenticated);
4429        assert!(poisoned.stake_table_key.is_none());
4430        assert_eq!(poisoned.stake, U256::from(10));
4431        assert!(AuthenticatedValidator::try_from(poisoned).is_err());
4432    }
4433
4434    #[test]
4435    fn test_register_v3_zero_bls_key_unauthenticated_in_from_l1_events() {
4436        let valid_val = TestValidator::random();
4437        let poison_val = TestValidator::random();
4438        let poison_account = poison_val.account;
4439
4440        let events = vec![
4441            StakeTableEvent::RegisterV3((&valid_val).into()),
4442            StakeTableEvent::RegisterV3(register_v3_with_bls(&poison_val, zero_g2())),
4443            StakeTableEvent::Delegate(Delegated {
4444                delegator: Address::random(),
4445                validator: poison_account,
4446                amount: U256::from(10),
4447            }),
4448        ];
4449
4450        let (validators, _) = validators_from_l1_events(events.into_iter()).expect("must not fail");
4451        let valid = validators
4452            .get(&valid_val.account)
4453            .expect("valid validator present");
4454        assert!(valid.authenticated);
4455
4456        let poisoned = validators
4457            .get(&poison_account)
4458            .expect("poison validator present as unauthenticated");
4459        assert!(!poisoned.authenticated);
4460        assert!(poisoned.stake_table_key.is_none());
4461        assert_eq!(poisoned.stake, U256::from(10));
4462        assert!(AuthenticatedValidator::try_from(poisoned).is_err());
4463    }
4464
4465    /// Two distinct off-curve registrations must both succeed; unparsable
4466    /// keys are never inserted into `used_bls_keys`.
4467    #[test]
4468    fn test_two_distinct_unparsable_g2_keys_both_register() {
4469        let zero = zero_g2();
4470        // x=(1, 0), y=(0, 0): violates y² = x³ + b' over Fp2.
4471        let nonzero = G2PointSol {
4472            x0: U256::from(1),
4473            x1: U256::ZERO,
4474            y0: U256::ZERO,
4475            y1: U256::ZERO,
4476        };
4477        assert!(BLSPubKey::try_from(zero).is_err());
4478        assert!(BLSPubKey::try_from(nonzero).is_err());
4479        assert_ne!(zero, nonzero);
4480
4481        let acc_a = Address::random();
4482        let acc_b = Address::random();
4483
4484        let mut state = StakeTableState::default();
4485        state
4486            .apply_event(StakeTableEvent::RegisterV2(ValidatorRegisteredV2 {
4487                account: acc_a,
4488                blsVK: zero,
4489                schnorrVK: StateKeyPair::generate().ver_key().into(),
4490                commission: 0,
4491                blsSig: zero_g1().into(),
4492                schnorrSig: Bytes::default(),
4493                metadataUri: String::new(),
4494            }))
4495            .expect("first unparsable register must not fail")
4496            .expect("first unparsable register must apply");
4497        state
4498            .apply_event(StakeTableEvent::RegisterV2(ValidatorRegisteredV2 {
4499                account: acc_b,
4500                blsVK: nonzero,
4501                schnorrVK: StateKeyPair::generate().ver_key().into(),
4502                commission: 0,
4503                blsSig: zero_g1().into(),
4504                schnorrSig: Bytes::default(),
4505                metadataUri: String::new(),
4506            }))
4507            .expect("second unparsable register must not fail")
4508            .expect("second unparsable register must apply");
4509
4510        for account in [acc_a, acc_b] {
4511            let v = state.validators().get(&account).expect("present");
4512            assert!(!v.authenticated);
4513            assert!(v.stake_table_key.is_none());
4514        }
4515        assert!(state.used_bls_keys().is_empty());
4516    }
4517
4518    /// Two distinct V3 off-curve registrations must both succeed; unparsable
4519    /// keys are never inserted into `used_bls_keys`. Each account uses a
4520    /// distinct nonzero x25519 key so the x25519 dedup check does not fire.
4521    #[test]
4522    fn test_register_v3_two_distinct_unparsable_g2_keys_both_register() {
4523        let zero = zero_g2();
4524        // x=(1, 0), y=(0, 0): violates y² = x³ + b' over Fp2.
4525        let nonzero = G2PointSol {
4526            x0: U256::from(1),
4527            x1: U256::ZERO,
4528            y0: U256::ZERO,
4529            y1: U256::ZERO,
4530        };
4531        assert!(BLSPubKey::try_from(zero).is_err());
4532        assert!(BLSPubKey::try_from(nonzero).is_err());
4533        assert_ne!(zero, nonzero);
4534
4535        let val_a = TestValidator::random();
4536        let val_b = TestValidator::random();
4537        let acc_a = val_a.account;
4538        let acc_b = val_b.account;
4539
4540        let mut state = StakeTableState::default();
4541        state
4542            .apply_event(StakeTableEvent::RegisterV3(register_v3_with_bls(
4543                &val_a, zero,
4544            )))
4545            .expect("first unparsable register must not fail")
4546            .expect("first unparsable register must apply");
4547        state
4548            .apply_event(StakeTableEvent::RegisterV3(register_v3_with_bls(
4549                &val_b, nonzero,
4550            )))
4551            .expect("second unparsable register must not fail")
4552            .expect("second unparsable register must apply");
4553
4554        for account in [acc_a, acc_b] {
4555            let v = state.validators().get(&account).expect("present");
4556            assert!(!v.authenticated);
4557            assert!(v.stake_table_key.is_none());
4558        }
4559        assert!(state.used_bls_keys().is_empty());
4560    }
4561
4562    /// After a poison Register, a subsequent valid KeyUpdateV2 must
4563    /// authenticate the validator and install the new keys.
4564    #[test]
4565    fn test_zero_g2_then_valid_key_update() {
4566        let mut state = StakeTableState::default();
4567        let valid_val = TestValidator::random();
4568        let account = valid_val.account;
4569
4570        state
4571            .apply_event(StakeTableEvent::RegisterV2(ValidatorRegisteredV2 {
4572                account,
4573                blsVK: zero_g2(),
4574                schnorrVK: StateKeyPair::generate().ver_key().into(),
4575                commission: 0,
4576                blsSig: zero_g1().into(),
4577                schnorrSig: Bytes::default(),
4578                metadataUri: String::new(),
4579            }))
4580            .unwrap()
4581            .unwrap();
4582        assert!(!state.validators().get(&account).unwrap().authenticated);
4583
4584        let new_keys = TestValidator::random_update_keys(account, 0);
4585        let update: ConsensusKeysUpdatedV2 = (&new_keys).into();
4586        state
4587            .apply_event(StakeTableEvent::KeyUpdateV2(update))
4588            .unwrap()
4589            .unwrap();
4590
4591        let v = state.validators().get(&account).unwrap();
4592        let expected_bls = BLSPubKey::try_from(new_keys.bls_vk).unwrap();
4593        assert_eq!(v.stake_table_key.as_ref(), Some(&expected_bls));
4594        // KeyUpdateV2 authenticates signatures, so on success the validator
4595        // is promoted to `authenticated=true`.
4596        assert!(v.authenticated);
4597    }
4598
4599    #[test]
4600    fn test_register_v3_zero_g2_then_valid_key_update() {
4601        let val = TestValidator::random();
4602        let account = val.account;
4603        let mut state = StakeTableState::default();
4604
4605        state
4606            .apply_event(StakeTableEvent::RegisterV3(register_v3_with_bls(
4607                &val,
4608                zero_g2(),
4609            )))
4610            .unwrap()
4611            .unwrap();
4612        assert!(!state.validators().get(&account).unwrap().authenticated);
4613
4614        let new_keys = TestValidator::random_update_keys(account, 0);
4615        let update: ConsensusKeysUpdatedV2 = (&new_keys).into();
4616        state
4617            .apply_event(StakeTableEvent::KeyUpdateV2(update))
4618            .unwrap()
4619            .unwrap();
4620
4621        let v = state.validators().get(&account).unwrap();
4622        let expected_bls = BLSPubKey::try_from(new_keys.bls_vk).unwrap();
4623        assert_eq!(v.stake_table_key.as_ref(), Some(&expected_bls));
4624        // KeyUpdateV2 authenticates signatures, so on success the validator
4625        // is promoted to `authenticated=true`.
4626        assert!(v.authenticated);
4627    }
4628
4629    #[test]
4630    fn test_key_update_bad_key_no_corruption() {
4631        // V1 path
4632        let mut state_v1 = StakeTableState::default();
4633        let val = TestValidator::random();
4634        state_v1
4635            .apply_event(StakeTableEvent::Register((&val).into()))
4636            .unwrap()
4637            .unwrap();
4638        let prior_v1 = state_v1
4639            .validators()
4640            .get(&val.account)
4641            .unwrap()
4642            .stake_table_key;
4643        assert_matches!(
4644            state_v1.apply_event(StakeTableEvent::KeyUpdate(ConsensusKeysUpdated {
4645                account: val.account,
4646                blsVK: zero_g2(),
4647                schnorrVK: StateKeyPair::generate().ver_key().into(),
4648            })),
4649            Ok(Err(ExpectedStakeTableError::InvalidBlsKey))
4650        );
4651        assert_eq!(
4652            state_v1
4653                .validators()
4654                .get(&val.account)
4655                .unwrap()
4656                .stake_table_key,
4657            prior_v1
4658        );
4659
4660        // V2 path: replay-from-persistence safety net; live fetch is pre-filtered.
4661        let mut state_v2 = StakeTableState::default();
4662        let val2 = TestValidator::random();
4663        state_v2
4664            .apply_event(StakeTableEvent::RegisterV2((&val2).into()))
4665            .unwrap()
4666            .unwrap();
4667        let prior_v2 = state_v2
4668            .validators()
4669            .get(&val2.account)
4670            .unwrap()
4671            .stake_table_key;
4672        assert_matches!(
4673            state_v2.apply_event(StakeTableEvent::KeyUpdateV2(ConsensusKeysUpdatedV2 {
4674                account: val2.account,
4675                blsVK: zero_g2(),
4676                schnorrVK: StateKeyPair::generate().ver_key().into(),
4677                blsSig: zero_g1().into(),
4678                schnorrSig: Bytes::default(),
4679            })),
4680            Ok(Err(ExpectedStakeTableError::InvalidBlsKey))
4681        );
4682        assert_eq!(
4683            state_v2
4684                .validators()
4685                .get(&val2.account)
4686                .unwrap()
4687                .stake_table_key,
4688            prior_v2
4689        );
4690    }
4691}
4692
4693// Deploys StakeTableV3 on anvil and fuzzes the property: Solidity accepts an x25519 key =>
4694// `apply_event` returns the outer `Ok(_)` and stores `parse_x25519_key(key).ok()`.
4695// See `parse_x25519_key` for why contract-rejected keys must still soft-fail.
4696#[cfg(test)]
4697mod proptest_x25519_key {
4698    use alloy::{
4699        primitives::FixedBytes,
4700        providers::{ProviderBuilder, WalletProvider},
4701    };
4702    use proptest::{
4703        prelude::*,
4704        test_runner::{Config as ProptestConfig, TestRunner},
4705    };
4706
4707    use super::{testing::TestValidator, *};
4708
4709    fn x25519_key_strategy() -> impl Strategy<Value = [u8; 32]> {
4710        // LE encoding of the curve25519 field prime p = 2^255 - 19.
4711        let mut p = [0xffu8; 32];
4712        p[0] = 0xed;
4713        p[31] = 0x7f;
4714        prop_oneof![
4715            // Uniform random bytes
4716            any::<[u8; 32]>(),
4717            // All zeros (contract rejects)
4718            Just([0u8; 32]),
4719            // Exactly p: nonzero but non-canonical
4720            Just(p),
4721            // The 19 non-canonical encodings p..2^255-1 (0xed + 18 = 0xff, so no overflow)
4722            (0..19u8).prop_map(move |k| {
4723                let mut key = p;
4724                key[0] += k;
4725                key
4726            }),
4727            // p - 1: largest canonical value
4728            Just({
4729                let mut key = p;
4730                key[0] = 0xec;
4731                key
4732            }),
4733            // 2^256 - 1: top bit set. Rust parses it (bit 255 is only masked during DH),
4734            // the contract rejects it as a non-canonical alias.
4735            Just([0xffu8; 32]),
4736            // Top bit only: same alias class as above
4737            Just({
4738                let mut key = [0u8; 32];
4739                key[31] = 0x80;
4740                key
4741            }),
4742            // Small integers, including low-order point encodings
4743            (0..=2u8).prop_map(|v| {
4744                let mut key = [0u8; 32];
4745                key[0] = v;
4746                key
4747            }),
4748        ]
4749    }
4750
4751    #[test]
4752    fn solidity_rust_x25519_validation_equivalence() {
4753        let rt = tokio::runtime::Runtime::new().unwrap();
4754
4755        let provider = ProviderBuilder::new().connect_anvil_with_wallet();
4756        let sender = provider.default_signer_address();
4757        let contract_addr = rt.block_on(async {
4758            let contract = StakeTableV3::deploy(&provider).await.unwrap();
4759            *contract.address()
4760        });
4761        let contract = StakeTableV3::new(contract_addr, &provider);
4762
4763        // Committed registration for the update-path oracle: gives `updateX25519Key` an
4764        // active validator. Done before both passes so every case sees identical state.
4765        let registered = TestValidator::random_update_keys(sender, 100).with_x25519_key([7u8; 32]);
4766        rt.block_on(async {
4767            let receipt = contract
4768                .registerValidatorV3(
4769                    registered.bls_vk,
4770                    registered.schnorr_vk,
4771                    registered.bls_sig.into(),
4772                    registered.schnorr_sig.clone(),
4773                    registered.commission,
4774                    String::new(),
4775                    FixedBytes(registered.x25519_key),
4776                    registered.p2p_addr.clone(),
4777                )
4778                .send()
4779                .await
4780                .unwrap()
4781                .get_receipt()
4782                .await
4783                .unwrap();
4784            assert!(receipt.status());
4785        });
4786
4787        // Rust mirror of the committed registration, base state for the update pass.
4788        let mut registered_state = StakeTableState::default();
4789        registered_state
4790            .apply_event(StakeTableEvent::RegisterV3((&registered).into()))
4791            .unwrap()
4792            .unwrap();
4793
4794        let base = TestValidator::random();
4795
4796        let cases = std::env::var("PROPTEST_CASES")
4797            .ok()
4798            .and_then(|v| v.parse().ok())
4799            .unwrap_or(512);
4800        let mut runner = TestRunner::new(ProptestConfig {
4801            cases,
4802            ..ProptestConfig::default()
4803        });
4804
4805        // Register path: static call so no state is committed between cases.
4806        runner
4807            .run(&x25519_key_strategy(), |key| {
4808                let sol_valid = rt.block_on(async {
4809                    contract
4810                        .registerValidatorV3(
4811                            base.bls_vk,
4812                            base.schnorr_vk,
4813                            base.bls_sig.into(),
4814                            base.schnorr_sig.clone(),
4815                            base.commission,
4816                            String::new(),
4817                            FixedBytes(key),
4818                            base.p2p_addr.clone(),
4819                        )
4820                        .from(base.account)
4821                        .call()
4822                        .await
4823                        .is_ok()
4824                });
4825
4826                let val = base.clone().with_x25519_key(key);
4827                let mut state = StakeTableState::default();
4828                let result = state.apply_event(StakeTableEvent::RegisterV3((&val).into()));
4829
4830                if sol_valid {
4831                    prop_assert!(
4832                        result.is_ok(),
4833                        "Solidity accepted registration with x25519 key {key:?} but apply_event \
4834                         failed: {result:?}"
4835                    );
4836                    let stored = state.validators().get(&val.account).unwrap().x25519_key;
4837                    // Contract-valid keys are always Rust-parsable, so `stored` must
4838                    // not degrade to `None`; the eq then pins the exact key.
4839                    prop_assert!(stored.is_some());
4840                    prop_assert_eq!(stored, parse_x25519_key(key).ok());
4841                }
4842                Ok(())
4843            })
4844            .unwrap();
4845
4846        // Update path: static call against the committed registration.
4847        runner
4848            .run(&x25519_key_strategy(), |key| {
4849                let sol_valid = rt.block_on(async {
4850                    contract
4851                        .updateX25519Key(FixedBytes(key))
4852                        .from(sender)
4853                        .call()
4854                        .await
4855                        .is_ok()
4856                });
4857
4858                let mut state = registered_state.clone();
4859                let result = state.apply_event(registered.x25519_update(key));
4860
4861                if sol_valid {
4862                    prop_assert!(
4863                        result.is_ok(),
4864                        "Solidity accepted x25519 key update {key:?} but apply_event failed: \
4865                         {result:?}"
4866                    );
4867                    let stored = state.validators().get(&sender).unwrap().x25519_key;
4868                    prop_assert!(stored.is_some());
4869                    prop_assert_eq!(stored, parse_x25519_key(key).ok());
4870                }
4871                Ok(())
4872            })
4873            .unwrap();
4874    }
4875}