Skip to main content

espresso_types/v0/v0_3/
stake_table.rs

1use std::{collections::HashMap, sync::Arc};
2
3use alloy::{
4    primitives::{Address, Log, U256},
5    transports::{RpcError, TransportErrorKind},
6};
7use async_lock::{Mutex, RwLock};
8use committable::{Commitment, Committable, RawCommitmentBuilder};
9use derive_more::derive::{From, Into};
10use hotshot_contract_adapter::sol_types::StakeTableV3::{
11    CommissionUpdated, ConsensusKeysUpdated, ConsensusKeysUpdatedV2, Delegated, P2pAddrUpdated,
12    Undelegated, UndelegatedV2, ValidatorExit, ValidatorExitV2, ValidatorRegistered,
13    ValidatorRegisteredV2, ValidatorRegisteredV3, X25519KeyUpdated,
14};
15use hotshot_types::{
16    PeerConfig,
17    addr::{InvalidNetAddr, NetAddr},
18    data::EpochNumber,
19    light_client::StateVerKey,
20    network::PeerConfigKeys,
21    traits::signature_key::SignatureKey,
22    x25519,
23};
24use itertools::Itertools;
25use jf_utils::to_bytes;
26use serde::{Deserialize, Serialize};
27use thiserror::Error;
28use tokio::task::JoinHandle;
29use vbs::version::Version;
30use versions::NEW_PROTOCOL_VERSION;
31
32#[cfg(feature = "node")]
33use super::L1Client;
34use crate::{
35    AuthenticatedValidatorMap, SeqTypes,
36    traits::{MembershipPersistence, StateCatchup},
37    v0::{ChainConfig, impls::StakeTableHash},
38    v0_3::RewardAmount,
39};
40/// Stake table holding all staking information (DA and non-DA stakers)
41#[derive(Debug, Clone, Serialize, Deserialize, From)]
42pub struct CombinedStakeTable(Vec<PeerConfigKeys<SeqTypes>>);
43
44#[derive(Clone, Debug, From, Into, Serialize, Deserialize, PartialEq, Eq)]
45/// NewType to disambiguate DA Membership
46pub struct DAMembers(pub Vec<PeerConfig<SeqTypes>>);
47
48#[derive(Clone, Debug, From, Into, Serialize, Deserialize, PartialEq, Eq)]
49/// NewType to disambiguate StakeTable
50pub struct StakeTable(pub Vec<PeerConfig<SeqTypes>>);
51
52pub(crate) fn to_fixed_bytes(value: U256) -> [u8; std::mem::size_of::<U256>()] {
53    let bytes: [u8; std::mem::size_of::<U256>()] = value.to_le_bytes();
54    bytes
55}
56
57/// Validator as registered in the stake table contract.
58///
59/// `stake_table_key` is `None` when the on-chain BLS key is unparsable
60/// (e.g. the Solidity all-zero G2 point); such validators are always
61/// unauthenticated and excluded from the active set.
62/// `state_ver_key` is `None` when the on-chain Schnorr key is unparsable
63/// (e.g. the all-zero EdOnBN254 point); such validators are likewise always
64/// unauthenticated and excluded from the active set.
65#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
66#[serde(bound(deserialize = ""))]
67pub struct RegisteredValidator<KEY: SignatureKey> {
68    pub account: Address,
69    /// The peer's public key
70    pub stake_table_key: Option<KEY>,
71    /// the peer's state public key
72    pub state_ver_key: Option<StateVerKey>,
73    /// the peer's stake
74    pub stake: U256,
75    // commission
76    // TODO: MA commission is only valid from 0 to 10_000. Add newtype to enforce this.
77    pub commission: u16,
78    pub delegators: HashMap<Address, U256>,
79    /// Whether the validator's registration signature has been verified.
80    /// Contract can verify BLS but only length-check Schnorr.
81    pub authenticated: bool,
82    /// Public X25519 key for network communication.
83    pub x25519_key: Option<x25519::PublicKey>,
84    /// Network address.
85    pub p2p_addr: Option<NetAddr>,
86}
87
88#[cfg(feature = "rlp")]
89mod registered_validator_rlp {
90    use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
91    use anyhow::Context;
92
93    use super::*;
94
95    #[derive(RlpEncodable, RlpDecodable, PartialEq, Eq, PartialOrd, Ord)]
96    struct DelegatorRlp {
97        address: Address,
98        stake: U256,
99    }
100
101    #[derive(RlpEncodable, RlpDecodable)]
102    #[rlp(trailing(canonical))]
103    struct RegisteredValidatorRlp {
104        account: Address,
105        stake: U256,
106        commission: u16,
107        delegators: Vec<DelegatorRlp>,
108        authenticated: bool,
109        stake_table_key: Option<Vec<u8>>,
110        state_ver_key: Option<Vec<u8>>,
111        x25519_key: Option<Vec<u8>>,
112        p2p_addr: Option<Vec<u8>>,
113    }
114
115    impl<KEY: SignatureKey> TryFrom<&RegisteredValidator<KEY>> for RegisteredValidatorRlp {
116        type Error = anyhow::Error;
117
118        fn try_from(v: &RegisteredValidator<KEY>) -> Result<Self, Self::Error> {
119            let mut delegators = v
120                .delegators
121                .iter()
122                .map(|(&address, &stake)| DelegatorRlp { address, stake })
123                .collect::<Vec<_>>();
124            // The order of delegators thus far depends on hashing. Sort to make the order
125            // canonical.
126            delegators.sort();
127
128            Ok(Self {
129                account: v.account,
130                stake: v.stake,
131                commission: v.commission,
132                delegators,
133                authenticated: v.authenticated,
134                stake_table_key: match &v.stake_table_key {
135                    Some(key) => {
136                        Some(bincode::serialize(key).context("serializing stake_table_key")?)
137                    },
138                    None => None,
139                },
140                state_ver_key: match &v.state_ver_key {
141                    Some(key) => {
142                        Some(bincode::serialize(key).context("serializing state_ver_key")?)
143                    },
144                    None => None,
145                },
146                x25519_key: match &v.x25519_key {
147                    Some(key) => Some(bincode::serialize(key).context("serializing x25519_key")?),
148                    None => None,
149                },
150                p2p_addr: match &v.p2p_addr {
151                    Some(addr) => Some(bincode::serialize(addr).context("serializing p2p2_addr")?),
152                    None => None,
153                },
154            })
155        }
156    }
157
158    impl<KEY: SignatureKey> TryFrom<RegisteredValidatorRlp> for RegisteredValidator<KEY> {
159        type Error = anyhow::Error;
160
161        fn try_from(v: RegisteredValidatorRlp) -> Result<Self, Self::Error> {
162            Ok(Self {
163                account: v.account,
164                stake: v.stake,
165                commission: v.commission,
166                delegators: v
167                    .delegators
168                    .into_iter()
169                    .map(|d| (d.address, d.stake))
170                    .collect(),
171                authenticated: v.authenticated,
172                stake_table_key: if let Some(bytes) = &v.stake_table_key {
173                    Some(bincode::deserialize(bytes).context("deserializing stake_table_key")?)
174                } else {
175                    None
176                },
177                state_ver_key: if let Some(bytes) = &v.state_ver_key {
178                    Some(bincode::deserialize(bytes).context("deserializing state_ver_key")?)
179                } else {
180                    None
181                },
182                x25519_key: if let Some(bytes) = &v.x25519_key {
183                    Some(bincode::deserialize(bytes).context("deserializing x25519_key")?)
184                } else {
185                    None
186                },
187                p2p_addr: if let Some(bytes) = &v.p2p_addr {
188                    Some(bincode::deserialize(bytes).context("deserializing p2p_addr")?)
189                } else {
190                    None
191                },
192            })
193        }
194    }
195
196    impl<KEY: SignatureKey> Encodable for RegisteredValidator<KEY> {
197        fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
198            RegisteredValidatorRlp::try_from(self)
199                .expect("failed to canonicalize RegisteredValidator")
200                .encode(out);
201        }
202
203        fn length(&self) -> usize {
204            RegisteredValidatorRlp::try_from(self)
205                .expect("failed to canonicalize RegisteredValidator")
206                .length()
207        }
208    }
209
210    impl<KEY: SignatureKey> Decodable for RegisteredValidator<KEY> {
211        fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
212            let rlp = RegisteredValidatorRlp::decode(buf)?;
213            rlp.try_into().map_err(|err| {
214                tracing::error!("failed to parse canonicalized RegisteredValidator: {err:#}");
215                alloy_rlp::Error::Custom("failed to parse canonicalized RegisteredValidator")
216            })
217        }
218    }
219}
220
221/// Validator eligible for consensus participation.
222/// Guaranteed to have valid BLS and Schnorr signatures.
223/// This is a newtype wrapper around RegisteredValidator that guarantees authenticated=true.
224#[derive(serde::Serialize, Clone, Debug, PartialEq, Eq)]
225pub struct AuthenticatedValidator<KEY: SignatureKey>(RegisteredValidator<KEY>);
226
227impl<'de, KEY: SignatureKey> Deserialize<'de> for AuthenticatedValidator<KEY> {
228    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
229    where
230        D: serde::Deserializer<'de>,
231    {
232        let inner = RegisteredValidator::deserialize(deserializer)?;
233        if !inner.authenticated {
234            return Err(serde::de::Error::custom(
235                "cannot deserialize unauthenticated validator as AuthenticatedValidator",
236            ));
237        }
238        if inner.stake_table_key.is_none() {
239            return Err(serde::de::Error::custom(
240                "cannot deserialize validator without BLS key as AuthenticatedValidator",
241            ));
242        }
243        if inner.state_ver_key.is_none() {
244            return Err(serde::de::Error::custom(
245                "cannot deserialize validator without Schnorr key as AuthenticatedValidator",
246            ));
247        }
248        Ok(AuthenticatedValidator(inner))
249    }
250}
251
252impl<KEY: SignatureKey> AuthenticatedValidator<KEY> {
253    pub fn into_inner(self) -> RegisteredValidator<KEY> {
254        self.0
255    }
256
257    /// Checks that this validator can participate in consensus at `protocol_version`.
258    ///
259    /// Encodes only protocol-version-gated requirements; stake and delegation checks
260    /// live in `select_active_validator_set`.
261    ///
262    /// The contract only checks the port of a p2p address, so the address is also
263    /// checked with [`NetAddr::validate`]: one that fails it cannot be dialed.
264    ///
265    /// # Liveness during the CLIQUENET upgrade transition
266    ///
267    /// The active validator set for epoch N is selected at the epoch root header in
268    /// epoch N-2, using that root header's protocol version (see `Fetcher::fetch`).
269    /// When CLIQUENET activates at some epoch K, the roots for epochs K and K+1 are
270    /// pre-CLIQUENET headers, so those active sets were selected without this filter
271    /// and may include validators missing `x25519_key` or `p2p_addr`.
272    ///
273    /// In epochs K and K+1, the cliquenet network silently skips peers without
274    /// connect info (no panic, no error), but they still count toward the quorum
275    /// threshold. Liveness through the transition therefore requires the eligible
276    /// subset of validators to hold >= 2/3 of total active stake. From epoch K+2
277    /// onward, roots are post-CLIQUENET and this filter applies.
278    ///
279    /// Operators are warned at startup on the version immediately preceding
280    /// NEW_PROTOCOL if their on-chain entry is missing network info.
281    pub fn ensure_eligible(&self, protocol_version: Version) -> Result<(), IneligibleValidator> {
282        if protocol_version < NEW_PROTOCOL_VERSION {
283            return Ok(());
284        }
285        if self.x25519_key.is_none() {
286            return Err(IneligibleValidator::MissingX25519Key);
287        }
288        let addr = self
289            .p2p_addr
290            .as_ref()
291            .ok_or(IneligibleValidator::MissingP2pAddr)?;
292        addr.validate()
293            .map_err(|source| IneligibleValidator::InvalidP2pAddr {
294                addr: addr.clone(),
295                source,
296            })
297    }
298
299    pub fn stake_table_key(&self) -> &KEY {
300        self.0
301            .stake_table_key
302            .as_ref()
303            .expect("AuthenticatedValidator invariant: key is Some")
304    }
305
306    pub fn state_ver_key(&self) -> &StateVerKey {
307        self.0
308            .state_ver_key
309            .as_ref()
310            .expect("AuthenticatedValidator invariant: state_ver_key is Some")
311    }
312}
313
314impl<KEY: SignatureKey> std::ops::Deref for AuthenticatedValidator<KEY> {
315    type Target = RegisteredValidator<KEY>;
316
317    fn deref(&self) -> &Self::Target {
318        &self.0
319    }
320}
321
322#[derive(Debug, Error)]
323#[error("Validator {0:#x} not authenticated (invalid registration signature)")]
324pub struct UnauthenticatedValidatorError(pub Address);
325
326/// Why [`AuthenticatedValidator::ensure_eligible`] keeps a validator out of the active set.
327#[derive(Debug, Error)]
328pub enum IneligibleValidator {
329    #[error("missing x25519 key")]
330    MissingX25519Key,
331    #[error("missing p2p address")]
332    MissingP2pAddr,
333    #[error("invalid p2p address {addr:?}: {source}")]
334    InvalidP2pAddr {
335        addr: NetAddr,
336        source: InvalidNetAddr,
337    },
338}
339
340impl<KEY: SignatureKey + Clone> TryFrom<&RegisteredValidator<KEY>> for AuthenticatedValidator<KEY> {
341    type Error = UnauthenticatedValidatorError;
342
343    fn try_from(v: &RegisteredValidator<KEY>) -> Result<Self, Self::Error> {
344        if !v.authenticated || v.stake_table_key.is_none() || v.state_ver_key.is_none() {
345            return Err(UnauthenticatedValidatorError(v.account));
346        }
347        Ok(AuthenticatedValidator(v.clone()))
348    }
349}
350
351impl<KEY: SignatureKey> TryFrom<RegisteredValidator<KEY>> for AuthenticatedValidator<KEY> {
352    type Error = UnauthenticatedValidatorError;
353
354    fn try_from(v: RegisteredValidator<KEY>) -> Result<Self, Self::Error> {
355        if !v.authenticated || v.stake_table_key.is_none() || v.state_ver_key.is_none() {
356            return Err(UnauthenticatedValidatorError(v.account));
357        }
358        Ok(AuthenticatedValidator(v))
359    }
360}
361
362impl<KEY: SignatureKey> From<AuthenticatedValidator<KEY>> for RegisteredValidator<KEY> {
363    fn from(v: AuthenticatedValidator<KEY>) -> Self {
364        v.into_inner()
365    }
366}
367
368impl<KEY: SignatureKey> Committable for RegisteredValidator<KEY> {
369    fn commit(&self) -> Commitment<Self> {
370        let mut builder =
371            RawCommitmentBuilder::new(&Self::tag()).fixed_size_field("account", &self.account);
372
373        // Present-key layout is preserved for backwards compatibility; absent
374        // keys use a distinct marker that can't collide with any present key.
375        builder = match &self.stake_table_key {
376            Some(key) => builder.var_size_field("stake_table_key", key.to_bytes().as_slice()),
377            None => builder.constant_str("no_bls_key"),
378        };
379
380        // Present-key layout is preserved for backwards compatibility; absent
381        // keys use a distinct marker that can't collide with any present key.
382        builder = match &self.state_ver_key {
383            Some(key) => builder.var_size_field("state_ver_key", &to_bytes!(key).unwrap()),
384            None => builder.constant_str("no_schnorr_key"),
385        };
386
387        builder = builder
388            .fixed_size_field("stake", &to_fixed_bytes(self.stake))
389            .constant_str("commission")
390            .u16(self.commission);
391
392        // x25519_key and p2p_addr are included in the commitment only when set.
393        // They are None until StakeTableV3 is deployed and the validator sets them.
394        // This maintains backwards compatibility with pre-V3 commitments.
395        if let Some(key) = &self.x25519_key {
396            builder = builder.var_size_field("x25519_key", key.as_slice());
397        }
398        if let Some(addr) = &self.p2p_addr {
399            builder = builder.var_size_field("p2p_addr", addr.unbracketed_string().as_bytes());
400        }
401
402        builder = builder.constant_str("delegators");
403        for (address, stake) in self.delegators.iter().sorted() {
404            builder = builder
405                .fixed_size_bytes(address)
406                .fixed_size_bytes(&to_fixed_bytes(*stake));
407        }
408
409        // Backwards compatibility: don't change the commitment of *authenticated* validators
410        if !self.authenticated {
411            builder = builder.constant_str("unauthenticated");
412        }
413
414        builder.finalize()
415    }
416
417    fn tag() -> String {
418        "VALIDATOR".to_string()
419    }
420}
421
422#[derive(serde::Serialize, serde::Deserialize, std::hash::Hash, Clone, Debug, PartialEq, Eq)]
423#[serde(bound(deserialize = ""))]
424pub struct Delegator {
425    pub address: Address,
426    pub validator: Address,
427    pub stake: U256,
428}
429
430/// Type for holding result sets matching epochs to stake tables.
431pub type IndexedStake = (
432    EpochNumber,
433    (AuthenticatedValidatorMap, Option<RewardAmount>),
434    Option<StakeTableHash>,
435);
436
437#[derive(Clone, derive_more::derive::Debug)]
438pub struct Fetcher {
439    /// Peers for catching up the stake table
440    #[debug(skip)]
441    pub(crate) peers: Arc<dyn StateCatchup>,
442    /// Methods for stake table persistence.
443    #[debug(skip)]
444    pub(crate) persistence: Arc<Mutex<dyn MembershipPersistence>>,
445    /// L1 provider
446    #[cfg(feature = "node")]
447    pub(crate) l1_client: L1Client,
448    /// Verifiable `ChainConfig` holding contract address
449    pub(crate) chain_config: Arc<Mutex<ChainConfig>>,
450    #[cfg_attr(not(feature = "node"), allow(dead_code))]
451    pub(crate) update_task: Arc<StakeTableUpdateTask>,
452    pub(crate) initial_supply: Arc<RwLock<Option<U256>>>,
453}
454
455#[derive(Debug, Default)]
456pub(crate) struct StakeTableUpdateTask(pub(crate) Mutex<Option<JoinHandle<()>>>);
457
458impl Drop for StakeTableUpdateTask {
459    fn drop(&mut self) {
460        if let Some(task) = self.0.get_mut().take() {
461            task.abort();
462        }
463    }
464}
465
466// (log block number, log index)
467pub type EventKey = (u64, u64);
468
469#[derive(Clone, derive_more::From, PartialEq, serde::Serialize, serde::Deserialize)]
470pub enum StakeTableEvent {
471    Register(ValidatorRegistered),
472    RegisterV2(ValidatorRegisteredV2),
473    Deregister(ValidatorExit),
474    DeregisterV2(ValidatorExitV2),
475    Delegate(Delegated),
476    Undelegate(Undelegated),
477    UndelegateV2(UndelegatedV2),
478    KeyUpdate(ConsensusKeysUpdated),
479    KeyUpdateV2(ConsensusKeysUpdatedV2),
480    CommissionUpdate(CommissionUpdated),
481    RegisterV3(ValidatorRegisteredV3),
482    X25519KeyUpdate(X25519KeyUpdated),
483    P2pAddrUpdate(P2pAddrUpdated),
484}
485
486#[derive(Debug, Error)]
487pub enum StakeTableError {
488    #[error("Validator {0:#x} already registered")]
489    AlreadyRegistered(Address),
490    #[error("Validator {0:#x} not found")]
491    ValidatorNotFound(Address),
492    #[error("Delegator {0:#x} not found")]
493    DelegatorNotFound(Address),
494    #[error("BLS key already used: {0}")]
495    BlsKeyAlreadyUsed(String),
496    #[error("Insufficient stake to undelegate")]
497    InsufficientStake,
498    #[error("Event authentication failed: {0}")]
499    AuthenticationFailed(String),
500    #[error("No validators met the minimum criteria (non-zero stake and at least one delegator)")]
501    NoValidValidators,
502    #[error("Could not compute maximum stake from filtered validators")]
503    MissingMaximumStake,
504    #[error("Overflow when calculating minimum stake threshold")]
505    MinimumStakeOverflow,
506    #[error("Delegator {0:#x} has 0 stake")]
507    ZeroDelegatorStake(Address),
508    #[error("Failed to hash stake table: {0}")]
509    HashError(#[from] bincode::Error),
510    #[error("Validator {0:#x} already exited and cannot be re-registered")]
511    ValidatorAlreadyExited(Address),
512    #[error("Validator {0:#x} has invalid commission {1}")]
513    InvalidCommission(Address, u16),
514    #[error("x25519 key already used: {0}")]
515    X25519KeyAlreadyUsed(String),
516    #[error("Invalid x25519 key: {0}")]
517    InvalidX25519Key(String),
518    #[error("Stake table event decode error {0}")]
519    StakeTableEventDecodeError(#[from] alloy::sol_types::Error),
520    #[error("Stake table events sorting error: {0}")]
521    EventSortingError(#[from] EventSortingError),
522}
523
524#[derive(Debug, Error)]
525pub enum ExpectedStakeTableError {
526    #[error("Schnorr key already used: {0}")]
527    SchnorrKeyAlreadyUsed(String),
528    #[error("Invalid BLS key")]
529    InvalidBlsKey,
530    #[error("Invalid Schnorr key")]
531    InvalidSchnorrKey,
532}
533
534#[derive(Debug, Error)]
535pub enum FetchRewardError {
536    #[error("No stake table contract address found in chain config")]
537    MissingStakeTableContract,
538
539    #[error("Token address fetch failed: {0}")]
540    TokenAddressFetch(#[source] alloy::contract::Error),
541
542    #[error("Token Initialized event logs are empty")]
543    MissingInitializedEvent,
544
545    #[error("Transaction hash not found in Initialized event log: {init_log:?}")]
546    MissingTransactionHash { init_log: Log },
547
548    #[error("Block number not found in Initialized event log")]
549    MissingBlockNumber,
550
551    #[error("Transfer event query failed: {0}")]
552    TransferEventQuery(#[source] alloy::contract::Error),
553
554    #[error("No Transfer event found in the Initialized event block")]
555    MissingTransferEvent,
556
557    #[error("Division by zero {0}")]
558    DivisionByZero(&'static str),
559
560    #[error("Overflow {0}")]
561    Overflow(&'static str),
562
563    #[error("Contract call failed: {0}")]
564    ContractCall(#[source] alloy::contract::Error),
565
566    #[error("Rpc call failed: {0}")]
567    Rpc(#[source] RpcError<TransportErrorKind>),
568
569    #[error("Exceeded max block range scan ({0} blocks) while searching for Initialized event")]
570    ExceededMaxScanRange(u64),
571
572    #[error("Scanning for Initialized event failed: {0}")]
573    ScanQueryFailed(#[source] alloy::contract::Error),
574}
575
576#[derive(Debug, thiserror::Error)]
577pub enum EventSortingError {
578    #[error("Missing block number in log")]
579    MissingBlockNumber,
580
581    #[error("Missing log index in log")]
582    MissingLogIndex,
583
584    #[error("Invalid stake table event")]
585    InvalidStakeTableEvent,
586}
587
588#[cfg(test)]
589mod tests {
590    use std::collections::HashMap;
591
592    use alloy::primitives::{Address, U256};
593    use committable::Committable;
594    use hotshot_types::{
595        addr::NetAddr, light_client::StateVerKey, signature_key::BLSPubKey,
596        traits::signature_key::SignatureKey, x25519,
597    };
598
599    use super::{AuthenticatedValidator, RegisteredValidator};
600
601    /// Both x25519_key and p2p_addr must independently affect the commitment.
602    #[test]
603    fn test_commitment_changes_with_x25519_and_p2p_fields() {
604        let base = RegisteredValidator::<BLSPubKey>::mock();
605        assert!(base.x25519_key.is_none());
606        assert!(base.p2p_addr.is_none());
607        let commit_base = base.commit();
608
609        let mut with_x25519 = base.clone();
610        with_x25519.x25519_key = Some(x25519::PublicKey::try_from([42u8; 32].as_slice()).unwrap());
611        let commit_x25519 = with_x25519.commit();
612
613        let mut with_p2p = base.clone();
614        with_p2p.p2p_addr = Some("127.0.0.1:8080".parse::<NetAddr>().unwrap());
615        let commit_p2p = with_p2p.commit();
616
617        assert_ne!(commit_base, commit_x25519);
618        assert_ne!(commit_base, commit_p2p);
619        assert_ne!(commit_x25519, commit_p2p);
620    }
621
622    #[test]
623    fn test_commitment_of_ipv6_addr_is_frozen() {
624        let p2p_addr: NetAddr = "[2001:db8::1]:9977".parse().unwrap();
625        assert_eq!(p2p_addr.unbracketed_string(), "2001:db8::1:9977");
626
627        let validator = RegisteredValidator::<BLSPubKey> {
628            account: Address::repeat_byte(7),
629            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([1u8; 32], 0).0),
630            state_ver_key: Some(StateVerKey::default()),
631            stake: U256::from(1000),
632            commission: 500,
633            delegators: HashMap::new(),
634            authenticated: true,
635            x25519_key: None,
636            p2p_addr: Some(p2p_addr),
637        };
638        assert_eq!(
639            validator.commit().to_string(),
640            "VALIDATOR~7K4UdfSAqHRUGHW6HvabPzbi7dnJxEbHfO66O3eO3Sbw"
641        );
642    }
643
644    /// Unauthenticated validators must produce a different commitment than authenticated ones.
645    /// This ensures validators with invalid signatures are distinguishable in the commitment tree.
646    #[test]
647    fn test_unauthenticated_validator_commitment_differs() {
648        let account = Address::random();
649        let stake_table_key = Some(BLSPubKey::generated_from_seed_indexed([1u8; 32], 0).0);
650        let state_ver_key = Some(StateVerKey::default());
651        let stake = U256::from(1000);
652        let commission = 500u16;
653        let delegators = HashMap::new();
654
655        let authenticated = RegisteredValidator {
656            account,
657            stake_table_key,
658            state_ver_key: state_ver_key.clone(),
659            stake,
660            commission,
661            delegators: delegators.clone(),
662            authenticated: true,
663            x25519_key: None,
664            p2p_addr: None,
665        };
666
667        let unauthenticated = RegisteredValidator {
668            account,
669            stake_table_key,
670            state_ver_key,
671            stake,
672            commission,
673            delegators,
674            authenticated: false,
675            x25519_key: None,
676            p2p_addr: None,
677        };
678
679        let auth_commitment = authenticated.commit();
680        let unauth_commitment = unauthenticated.commit();
681        assert_ne!(
682            auth_commitment.as_ref() as &[u8],
683            unauth_commitment.as_ref() as &[u8]
684        );
685    }
686
687    #[test]
688    fn test_registered_validator_serde_roundtrip_with_none_key() {
689        let v: RegisteredValidator<BLSPubKey> = RegisteredValidator {
690            account: Address::random(),
691            stake_table_key: None,
692            state_ver_key: Some(StateVerKey::default()),
693            stake: U256::from(42u64),
694            commission: 1000,
695            delegators: HashMap::new(),
696            authenticated: false,
697            x25519_key: None,
698            p2p_addr: None,
699        };
700        let json = serde_json::to_string(&v).unwrap();
701        let v2: RegisteredValidator<BLSPubKey> = serde_json::from_str(&json).unwrap();
702        assert_eq!(v, v2);
703        assert!(v2.stake_table_key.is_none());
704    }
705
706    #[test]
707    fn test_authenticated_try_from_rejects_none_key() {
708        let v: RegisteredValidator<BLSPubKey> = RegisteredValidator {
709            account: Address::random(),
710            stake_table_key: None,
711            state_ver_key: Some(StateVerKey::default()),
712            stake: U256::from(1u64),
713            commission: 0,
714            delegators: HashMap::new(),
715            authenticated: true,
716            x25519_key: None,
717            p2p_addr: None,
718        };
719        assert!(AuthenticatedValidator::try_from(&v).is_err());
720        assert!(AuthenticatedValidator::try_from(v).is_err());
721    }
722
723    #[test]
724    fn test_authenticated_try_from_rejects_none_schnorr_key() {
725        let v: RegisteredValidator<BLSPubKey> = RegisteredValidator {
726            account: Address::random(),
727            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([1u8; 32], 0).0),
728            state_ver_key: None,
729            stake: U256::from(1u64),
730            commission: 0,
731            delegators: HashMap::new(),
732            authenticated: true,
733            x25519_key: None,
734            p2p_addr: None,
735        };
736        assert!(AuthenticatedValidator::try_from(&v).is_err());
737        assert!(AuthenticatedValidator::try_from(v).is_err());
738    }
739
740    #[test]
741    fn test_commit_none_vs_some_differs() {
742        let account = Address::random();
743        let state_ver_key = Some(StateVerKey::default());
744        let stake = U256::from(7u64);
745
746        let with_key: RegisteredValidator<BLSPubKey> = RegisteredValidator {
747            account,
748            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([2u8; 32], 0).0),
749            state_ver_key: state_ver_key.clone(),
750            stake,
751            commission: 100,
752            delegators: HashMap::new(),
753            authenticated: false,
754            x25519_key: None,
755            p2p_addr: None,
756        };
757        let without_key: RegisteredValidator<BLSPubKey> = RegisteredValidator {
758            account,
759            stake_table_key: None,
760            state_ver_key,
761            stake,
762            commission: 100,
763            delegators: HashMap::new(),
764            authenticated: false,
765            x25519_key: None,
766            p2p_addr: None,
767        };
768        assert_ne!(
769            with_key.commit().as_ref() as &[u8],
770            without_key.commit().as_ref() as &[u8]
771        );
772    }
773
774    #[test]
775    fn test_commit_none_vs_some_schnorr_differs() {
776        let account = Address::random();
777        let stake_table_key = Some(BLSPubKey::generated_from_seed_indexed([3u8; 32], 0).0);
778        let stake = U256::from(11u64);
779
780        let with_schnorr: RegisteredValidator<BLSPubKey> = RegisteredValidator {
781            account,
782            stake_table_key,
783            state_ver_key: Some(StateVerKey::default()),
784            stake,
785            commission: 100,
786            delegators: HashMap::new(),
787            authenticated: false,
788            x25519_key: None,
789            p2p_addr: None,
790        };
791        let without_schnorr: RegisteredValidator<BLSPubKey> = RegisteredValidator {
792            account,
793            stake_table_key,
794            state_ver_key: None,
795            stake,
796            commission: 100,
797            delegators: HashMap::new(),
798            authenticated: false,
799            x25519_key: None,
800            p2p_addr: None,
801        };
802        assert_ne!(
803            with_schnorr.commit().as_ref() as &[u8],
804            without_schnorr.commit().as_ref() as &[u8]
805        );
806    }
807}
808
809#[cfg(all(test, feature = "rlp"))]
810mod rlp_tests {
811    use alloy_rlp::{Decodable, Encodable};
812    use hotshot_types::{signature_key::BLSPubKey, traits::signature_key::StateSignatureKey};
813
814    use super::*;
815
816    #[test]
817    fn test_registered_validator_round_trip() {
818        let seed = [0; 32];
819        let validator = RegisteredValidator {
820            account: Address::random(),
821            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed(seed, 0).0),
822            state_ver_key: Some(StateVerKey::generated_from_seed_indexed(seed, 0).0),
823            stake: U256::MAX,
824            commission: u16::MAX,
825            delegators: [
826                (Address::random(), U256::ZERO),
827                (Address::random(), U256::MAX),
828            ]
829            .into_iter()
830            .collect(),
831            authenticated: true,
832            x25519_key: Some(x25519::Keypair::generate().unwrap().public_key()),
833            p2p_addr: Some(NetAddr::named("localhost", 8080)),
834        };
835
836        let mut bytes = vec![];
837        validator.encode(&mut bytes);
838        assert_eq!(bytes.len(), validator.length());
839        assert_eq!(
840            validator,
841            RegisteredValidator::decode(&mut bytes.as_slice()).unwrap()
842        );
843    }
844
845    #[test]
846    fn test_registered_validator_round_trip_empty() {
847        let validator = RegisteredValidator::<BLSPubKey> {
848            account: Address::random(),
849            stake_table_key: None,
850            state_ver_key: None,
851            stake: U256::MAX,
852            commission: u16::MAX,
853            delegators: Default::default(),
854            authenticated: true,
855            x25519_key: None,
856            p2p_addr: None,
857        };
858
859        let mut bytes = vec![];
860        validator.encode(&mut bytes);
861        assert_eq!(bytes.len(), validator.length());
862        assert_eq!(
863            validator,
864            RegisteredValidator::decode(&mut bytes.as_slice()).unwrap()
865        );
866    }
867
868    #[test]
869    fn test_registered_validator_round_canonical_order() {
870        // Two hashmaps with different capacities are very likely to end up with a different
871        // iteration order.
872        let mut delegators1 = HashMap::with_capacity(100);
873        let mut delegators2 = HashMap::with_capacity(10_000);
874        for i in 0..100 {
875            let delegator = Address::random();
876            let amount = U256::from(i);
877            delegators1.insert(delegator, amount);
878            delegators2.insert(delegator, amount);
879        }
880        assert_eq!(delegators1, delegators2);
881        assert_ne!(
882            delegators1.iter().collect::<Vec<_>>(),
883            delegators2.iter().collect::<Vec<_>>()
884        );
885
886        let validator1 = RegisteredValidator::<BLSPubKey> {
887            account: Address::random(),
888            stake_table_key: None,
889            state_ver_key: None,
890            stake: U256::MAX,
891            commission: u16::MAX,
892            delegators: delegators1,
893            authenticated: true,
894            x25519_key: None,
895            p2p_addr: None,
896        };
897        let validator2 = RegisteredValidator::<BLSPubKey> {
898            delegators: delegators2,
899            ..validator1.clone()
900        };
901        assert_eq!(validator1, validator2);
902
903        let mut bytes1 = vec![];
904        validator1.encode(&mut bytes1);
905        assert_eq!(bytes1.len(), validator1.length());
906
907        let mut bytes2 = vec![];
908        validator2.encode(&mut bytes2);
909        assert_eq!(bytes2.len(), validator2.length());
910
911        assert_eq!(bytes1, bytes2);
912        assert_eq!(
913            validator1,
914            RegisteredValidator::decode(&mut bytes2.as_slice()).unwrap()
915        );
916        assert_eq!(
917            validator2,
918            RegisteredValidator::decode(&mut bytes1.as_slice()).unwrap()
919        );
920    }
921}