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, addr::NetAddr, data::EpochNumber, light_client::StateVerKey,
17    network::PeerConfigKeys, traits::signature_key::SignatureKey, x25519,
18};
19use itertools::Itertools;
20use jf_utils::to_bytes;
21use serde::{Deserialize, Serialize};
22use thiserror::Error;
23use tokio::task::JoinHandle;
24use vbs::version::Version;
25use versions::NEW_PROTOCOL_VERSION;
26
27#[cfg(feature = "node")]
28use super::L1Client;
29use crate::{
30    AuthenticatedValidatorMap, SeqTypes,
31    traits::{MembershipPersistence, StateCatchup},
32    v0::{ChainConfig, impls::StakeTableHash},
33    v0_3::RewardAmount,
34};
35/// Stake table holding all staking information (DA and non-DA stakers)
36#[derive(Debug, Clone, Serialize, Deserialize, From)]
37pub struct CombinedStakeTable(Vec<PeerConfigKeys<SeqTypes>>);
38
39#[derive(Clone, Debug, From, Into, Serialize, Deserialize, PartialEq, Eq)]
40/// NewType to disambiguate DA Membership
41pub struct DAMembers(pub Vec<PeerConfig<SeqTypes>>);
42
43#[derive(Clone, Debug, From, Into, Serialize, Deserialize, PartialEq, Eq)]
44/// NewType to disambiguate StakeTable
45pub struct StakeTable(pub Vec<PeerConfig<SeqTypes>>);
46
47pub(crate) fn to_fixed_bytes(value: U256) -> [u8; std::mem::size_of::<U256>()] {
48    let bytes: [u8; std::mem::size_of::<U256>()] = value.to_le_bytes();
49    bytes
50}
51
52/// Validator as registered in the stake table contract.
53///
54/// `stake_table_key` is `None` when the on-chain BLS key is unparsable
55/// (e.g. the Solidity all-zero G2 point); such validators are always
56/// unauthenticated and excluded from the active set.
57/// `state_ver_key` is `None` when the on-chain Schnorr key is unparsable
58/// (e.g. the all-zero EdOnBN254 point); such validators are likewise always
59/// unauthenticated and excluded from the active set.
60#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
61#[serde(bound(deserialize = ""))]
62pub struct RegisteredValidator<KEY: SignatureKey> {
63    pub account: Address,
64    /// The peer's public key
65    pub stake_table_key: Option<KEY>,
66    /// the peer's state public key
67    pub state_ver_key: Option<StateVerKey>,
68    /// the peer's stake
69    pub stake: U256,
70    // commission
71    // TODO: MA commission is only valid from 0 to 10_000. Add newtype to enforce this.
72    pub commission: u16,
73    pub delegators: HashMap<Address, U256>,
74    /// Whether the validator's registration signature has been verified.
75    /// Contract can verify BLS but only length-check Schnorr.
76    pub authenticated: bool,
77    /// Public X25519 key for network communication.
78    pub x25519_key: Option<x25519::PublicKey>,
79    /// Network address.
80    pub p2p_addr: Option<NetAddr>,
81}
82
83/// Validator eligible for consensus participation.
84/// Guaranteed to have valid BLS and Schnorr signatures.
85/// This is a newtype wrapper around RegisteredValidator that guarantees authenticated=true.
86#[derive(serde::Serialize, Clone, Debug, PartialEq, Eq)]
87pub struct AuthenticatedValidator<KEY: SignatureKey>(RegisteredValidator<KEY>);
88
89impl<'de, KEY: SignatureKey> Deserialize<'de> for AuthenticatedValidator<KEY> {
90    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
91    where
92        D: serde::Deserializer<'de>,
93    {
94        let inner = RegisteredValidator::deserialize(deserializer)?;
95        if !inner.authenticated {
96            return Err(serde::de::Error::custom(
97                "cannot deserialize unauthenticated validator as AuthenticatedValidator",
98            ));
99        }
100        if inner.stake_table_key.is_none() {
101            return Err(serde::de::Error::custom(
102                "cannot deserialize validator without BLS key as AuthenticatedValidator",
103            ));
104        }
105        if inner.state_ver_key.is_none() {
106            return Err(serde::de::Error::custom(
107                "cannot deserialize validator without Schnorr key as AuthenticatedValidator",
108            ));
109        }
110        Ok(AuthenticatedValidator(inner))
111    }
112}
113
114impl<KEY: SignatureKey> AuthenticatedValidator<KEY> {
115    pub fn into_inner(self) -> RegisteredValidator<KEY> {
116        self.0
117    }
118
119    /// Whether this validator can participate in consensus at `protocol_version`.
120    ///
121    /// Encodes only protocol-version-gated requirements; stake and delegation checks
122    /// live in `select_active_validator_set`.
123    ///
124    /// # Liveness during the CLIQUENET upgrade transition
125    ///
126    /// The active validator set for epoch N is selected at the epoch root header in
127    /// epoch N-2, using that root header's protocol version (see `Fetcher::fetch`).
128    /// When CLIQUENET activates at some epoch K, the roots for epochs K and K+1 are
129    /// pre-CLIQUENET headers, so those active sets were selected without this filter
130    /// and may include validators missing `x25519_key` or `p2p_addr`.
131    ///
132    /// In epochs K and K+1, the cliquenet network silently skips peers without
133    /// connect info (no panic, no error), but they still count toward the quorum
134    /// threshold. Liveness through the transition therefore requires the eligible
135    /// subset of validators to hold >= 2/3 of total active stake. From epoch K+2
136    /// onward, roots are post-CLIQUENET and this filter applies.
137    ///
138    /// Operators are warned at startup on the version immediately preceding
139    /// NEW_PROTOCOL if their on-chain entry is missing network info.
140    pub fn is_eligible(&self, protocol_version: Version) -> bool {
141        if protocol_version >= NEW_PROTOCOL_VERSION
142            && (self.x25519_key.is_none() || self.p2p_addr.is_none())
143        {
144            return false;
145        }
146        true
147    }
148
149    pub fn stake_table_key(&self) -> &KEY {
150        self.0
151            .stake_table_key
152            .as_ref()
153            .expect("AuthenticatedValidator invariant: key is Some")
154    }
155
156    pub fn state_ver_key(&self) -> &StateVerKey {
157        self.0
158            .state_ver_key
159            .as_ref()
160            .expect("AuthenticatedValidator invariant: state_ver_key is Some")
161    }
162}
163
164impl<KEY: SignatureKey> std::ops::Deref for AuthenticatedValidator<KEY> {
165    type Target = RegisteredValidator<KEY>;
166
167    fn deref(&self) -> &Self::Target {
168        &self.0
169    }
170}
171
172#[derive(Debug, Error)]
173#[error("Validator {0:#x} not authenticated (invalid registration signature)")]
174pub struct UnauthenticatedValidatorError(pub Address);
175
176impl<KEY: SignatureKey + Clone> TryFrom<&RegisteredValidator<KEY>> for AuthenticatedValidator<KEY> {
177    type Error = UnauthenticatedValidatorError;
178
179    fn try_from(v: &RegisteredValidator<KEY>) -> Result<Self, Self::Error> {
180        if !v.authenticated || v.stake_table_key.is_none() || v.state_ver_key.is_none() {
181            return Err(UnauthenticatedValidatorError(v.account));
182        }
183        Ok(AuthenticatedValidator(v.clone()))
184    }
185}
186
187impl<KEY: SignatureKey> TryFrom<RegisteredValidator<KEY>> for AuthenticatedValidator<KEY> {
188    type Error = UnauthenticatedValidatorError;
189
190    fn try_from(v: RegisteredValidator<KEY>) -> Result<Self, Self::Error> {
191        if !v.authenticated || v.stake_table_key.is_none() || v.state_ver_key.is_none() {
192            return Err(UnauthenticatedValidatorError(v.account));
193        }
194        Ok(AuthenticatedValidator(v))
195    }
196}
197
198impl<KEY: SignatureKey> From<AuthenticatedValidator<KEY>> for RegisteredValidator<KEY> {
199    fn from(v: AuthenticatedValidator<KEY>) -> Self {
200        v.into_inner()
201    }
202}
203
204impl<KEY: SignatureKey> Committable for RegisteredValidator<KEY> {
205    fn commit(&self) -> Commitment<Self> {
206        let mut builder =
207            RawCommitmentBuilder::new(&Self::tag()).fixed_size_field("account", &self.account);
208
209        // Present-key layout is preserved for backwards compatibility; absent
210        // keys use a distinct marker that can't collide with any present key.
211        builder = match &self.stake_table_key {
212            Some(key) => builder.var_size_field("stake_table_key", key.to_bytes().as_slice()),
213            None => builder.constant_str("no_bls_key"),
214        };
215
216        // Present-key layout is preserved for backwards compatibility; absent
217        // keys use a distinct marker that can't collide with any present key.
218        builder = match &self.state_ver_key {
219            Some(key) => builder.var_size_field("state_ver_key", &to_bytes!(key).unwrap()),
220            None => builder.constant_str("no_schnorr_key"),
221        };
222
223        builder = builder
224            .fixed_size_field("stake", &to_fixed_bytes(self.stake))
225            .constant_str("commission")
226            .u16(self.commission);
227
228        // x25519_key and p2p_addr are included in the commitment only when set.
229        // They are None until StakeTableV3 is deployed and the validator sets them.
230        // This maintains backwards compatibility with pre-V3 commitments.
231        if let Some(key) = &self.x25519_key {
232            builder = builder.var_size_field("x25519_key", key.as_slice());
233        }
234        if let Some(addr) = &self.p2p_addr {
235            builder = builder.var_size_field("p2p_addr", addr.to_string().as_bytes());
236        }
237
238        builder = builder.constant_str("delegators");
239        for (address, stake) in self.delegators.iter().sorted() {
240            builder = builder
241                .fixed_size_bytes(address)
242                .fixed_size_bytes(&to_fixed_bytes(*stake));
243        }
244
245        // Backwards compatibility: don't change the commitment of *authenticated* validators
246        if !self.authenticated {
247            builder = builder.constant_str("unauthenticated");
248        }
249
250        builder.finalize()
251    }
252
253    fn tag() -> String {
254        "VALIDATOR".to_string()
255    }
256}
257
258#[derive(serde::Serialize, serde::Deserialize, std::hash::Hash, Clone, Debug, PartialEq, Eq)]
259#[serde(bound(deserialize = ""))]
260pub struct Delegator {
261    pub address: Address,
262    pub validator: Address,
263    pub stake: U256,
264}
265
266/// Type for holding result sets matching epochs to stake tables.
267pub type IndexedStake = (
268    EpochNumber,
269    (AuthenticatedValidatorMap, Option<RewardAmount>),
270    Option<StakeTableHash>,
271);
272
273#[derive(Clone, derive_more::derive::Debug)]
274pub struct Fetcher {
275    /// Peers for catching up the stake table
276    #[debug(skip)]
277    pub(crate) peers: Arc<dyn StateCatchup>,
278    /// Methods for stake table persistence.
279    #[debug(skip)]
280    pub(crate) persistence: Arc<Mutex<dyn MembershipPersistence>>,
281    /// L1 provider
282    #[cfg(feature = "node")]
283    pub(crate) l1_client: L1Client,
284    /// Verifiable `ChainConfig` holding contract address
285    pub(crate) chain_config: Arc<Mutex<ChainConfig>>,
286    #[cfg_attr(not(feature = "node"), allow(dead_code))]
287    pub(crate) update_task: Arc<StakeTableUpdateTask>,
288    pub initial_supply: Arc<RwLock<Option<U256>>>,
289}
290
291#[derive(Debug, Default)]
292pub(crate) struct StakeTableUpdateTask(pub(crate) Mutex<Option<JoinHandle<()>>>);
293
294impl Drop for StakeTableUpdateTask {
295    fn drop(&mut self) {
296        if let Some(task) = self.0.get_mut().take() {
297            task.abort();
298        }
299    }
300}
301
302// (log block number, log index)
303pub type EventKey = (u64, u64);
304
305#[derive(Clone, derive_more::From, PartialEq, serde::Serialize, serde::Deserialize)]
306pub enum StakeTableEvent {
307    Register(ValidatorRegistered),
308    RegisterV2(ValidatorRegisteredV2),
309    Deregister(ValidatorExit),
310    DeregisterV2(ValidatorExitV2),
311    Delegate(Delegated),
312    Undelegate(Undelegated),
313    UndelegateV2(UndelegatedV2),
314    KeyUpdate(ConsensusKeysUpdated),
315    KeyUpdateV2(ConsensusKeysUpdatedV2),
316    CommissionUpdate(CommissionUpdated),
317    RegisterV3(ValidatorRegisteredV3),
318    X25519KeyUpdate(X25519KeyUpdated),
319    P2pAddrUpdate(P2pAddrUpdated),
320}
321
322#[derive(Debug, Error)]
323pub enum StakeTableError {
324    #[error("Validator {0:#x} already registered")]
325    AlreadyRegistered(Address),
326    #[error("Validator {0:#x} not found")]
327    ValidatorNotFound(Address),
328    #[error("Delegator {0:#x} not found")]
329    DelegatorNotFound(Address),
330    #[error("BLS key already used: {0}")]
331    BlsKeyAlreadyUsed(String),
332    #[error("Insufficient stake to undelegate")]
333    InsufficientStake,
334    #[error("Event authentication failed: {0}")]
335    AuthenticationFailed(String),
336    #[error("No validators met the minimum criteria (non-zero stake and at least one delegator)")]
337    NoValidValidators,
338    #[error("Could not compute maximum stake from filtered validators")]
339    MissingMaximumStake,
340    #[error("Overflow when calculating minimum stake threshold")]
341    MinimumStakeOverflow,
342    #[error("Delegator {0:#x} has 0 stake")]
343    ZeroDelegatorStake(Address),
344    #[error("Failed to hash stake table: {0}")]
345    HashError(#[from] bincode::Error),
346    #[error("Validator {0:#x} already exited and cannot be re-registered")]
347    ValidatorAlreadyExited(Address),
348    #[error("Validator {0:#x} has invalid commission {1}")]
349    InvalidCommission(Address, u16),
350    #[error("Schnorr key already used: {0}")]
351    SchnorrKeyAlreadyUsed(String),
352    #[error("x25519 key already used: {0}")]
353    X25519KeyAlreadyUsed(String),
354    #[error("Invalid x25519 key: {0}")]
355    InvalidX25519Key(String),
356    #[error("Stake table event decode error {0}")]
357    StakeTableEventDecodeError(#[from] alloy::sol_types::Error),
358    #[error("Stake table events sorting error: {0}")]
359    EventSortingError(#[from] EventSortingError),
360}
361
362#[derive(Debug, Error)]
363pub enum ExpectedStakeTableError {
364    #[error("Schnorr key already used: {0}")]
365    SchnorrKeyAlreadyUsed(String),
366    #[error("Invalid BLS key")]
367    InvalidBlsKey,
368    #[error("Invalid Schnorr key")]
369    InvalidSchnorrKey,
370}
371
372#[derive(Debug, Error)]
373pub enum FetchRewardError {
374    #[error("No stake table contract address found in chain config")]
375    MissingStakeTableContract,
376
377    #[error("Token address fetch failed: {0}")]
378    TokenAddressFetch(#[source] alloy::contract::Error),
379
380    #[error("Token Initialized event logs are empty")]
381    MissingInitializedEvent,
382
383    #[error("Transaction hash not found in Initialized event log: {init_log:?}")]
384    MissingTransactionHash { init_log: Log },
385
386    #[error("Block number not found in Initialized event log")]
387    MissingBlockNumber,
388
389    #[error("Transfer event query failed: {0}")]
390    TransferEventQuery(#[source] alloy::contract::Error),
391
392    #[error("No Transfer event found in the Initialized event block")]
393    MissingTransferEvent,
394
395    #[error("Division by zero {0}")]
396    DivisionByZero(&'static str),
397
398    #[error("Overflow {0}")]
399    Overflow(&'static str),
400
401    #[error("Contract call failed: {0}")]
402    ContractCall(#[source] alloy::contract::Error),
403
404    #[error("Rpc call failed: {0}")]
405    Rpc(#[source] RpcError<TransportErrorKind>),
406
407    #[error("Exceeded max block range scan ({0} blocks) while searching for Initialized event")]
408    ExceededMaxScanRange(u64),
409
410    #[error("Scanning for Initialized event failed: {0}")]
411    ScanQueryFailed(#[source] alloy::contract::Error),
412}
413
414#[derive(Debug, thiserror::Error)]
415pub enum EventSortingError {
416    #[error("Missing block number in log")]
417    MissingBlockNumber,
418
419    #[error("Missing log index in log")]
420    MissingLogIndex,
421
422    #[error("Invalid stake table event")]
423    InvalidStakeTableEvent,
424}
425
426#[cfg(test)]
427mod tests {
428    use std::collections::HashMap;
429
430    use alloy::primitives::{Address, U256};
431    use committable::Committable;
432    use hotshot::types::{BLSPubKey, SignatureKey};
433    use hotshot_types::{addr::NetAddr, light_client::StateVerKey, x25519};
434
435    use super::{AuthenticatedValidator, RegisteredValidator};
436
437    /// Both x25519_key and p2p_addr must independently affect the commitment.
438    #[test]
439    fn test_commitment_changes_with_x25519_and_p2p_fields() {
440        let base = RegisteredValidator::<BLSPubKey>::mock();
441        assert!(base.x25519_key.is_none());
442        assert!(base.p2p_addr.is_none());
443        let commit_base = base.commit();
444
445        let mut with_x25519 = base.clone();
446        with_x25519.x25519_key = Some(x25519::PublicKey::try_from([42u8; 32].as_slice()).unwrap());
447        let commit_x25519 = with_x25519.commit();
448
449        let mut with_p2p = base.clone();
450        with_p2p.p2p_addr = Some("127.0.0.1:8080".parse::<NetAddr>().unwrap());
451        let commit_p2p = with_p2p.commit();
452
453        assert_ne!(commit_base, commit_x25519);
454        assert_ne!(commit_base, commit_p2p);
455        assert_ne!(commit_x25519, commit_p2p);
456    }
457
458    /// Unauthenticated validators must produce a different commitment than authenticated ones.
459    /// This ensures validators with invalid signatures are distinguishable in the commitment tree.
460    #[test]
461    fn test_unauthenticated_validator_commitment_differs() {
462        let account = Address::random();
463        let stake_table_key = Some(BLSPubKey::generated_from_seed_indexed([1u8; 32], 0).0);
464        let state_ver_key = Some(StateVerKey::default());
465        let stake = U256::from(1000);
466        let commission = 500u16;
467        let delegators = HashMap::new();
468
469        let authenticated = RegisteredValidator {
470            account,
471            stake_table_key,
472            state_ver_key: state_ver_key.clone(),
473            stake,
474            commission,
475            delegators: delegators.clone(),
476            authenticated: true,
477            x25519_key: None,
478            p2p_addr: None,
479        };
480
481        let unauthenticated = RegisteredValidator {
482            account,
483            stake_table_key,
484            state_ver_key,
485            stake,
486            commission,
487            delegators,
488            authenticated: false,
489            x25519_key: None,
490            p2p_addr: None,
491        };
492
493        let auth_commitment = authenticated.commit();
494        let unauth_commitment = unauthenticated.commit();
495        assert_ne!(
496            auth_commitment.as_ref() as &[u8],
497            unauth_commitment.as_ref() as &[u8]
498        );
499    }
500
501    #[test]
502    fn test_registered_validator_serde_roundtrip_with_none_key() {
503        let v: RegisteredValidator<BLSPubKey> = RegisteredValidator {
504            account: Address::random(),
505            stake_table_key: None,
506            state_ver_key: Some(StateVerKey::default()),
507            stake: U256::from(42u64),
508            commission: 1000,
509            delegators: HashMap::new(),
510            authenticated: false,
511            x25519_key: None,
512            p2p_addr: None,
513        };
514        let json = serde_json::to_string(&v).unwrap();
515        let v2: RegisteredValidator<BLSPubKey> = serde_json::from_str(&json).unwrap();
516        assert_eq!(v, v2);
517        assert!(v2.stake_table_key.is_none());
518    }
519
520    #[test]
521    fn test_authenticated_try_from_rejects_none_key() {
522        let v: RegisteredValidator<BLSPubKey> = RegisteredValidator {
523            account: Address::random(),
524            stake_table_key: None,
525            state_ver_key: Some(StateVerKey::default()),
526            stake: U256::from(1u64),
527            commission: 0,
528            delegators: HashMap::new(),
529            authenticated: true,
530            x25519_key: None,
531            p2p_addr: None,
532        };
533        assert!(AuthenticatedValidator::try_from(&v).is_err());
534        assert!(AuthenticatedValidator::try_from(v).is_err());
535    }
536
537    #[test]
538    fn test_authenticated_try_from_rejects_none_schnorr_key() {
539        let v: RegisteredValidator<BLSPubKey> = RegisteredValidator {
540            account: Address::random(),
541            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([1u8; 32], 0).0),
542            state_ver_key: None,
543            stake: U256::from(1u64),
544            commission: 0,
545            delegators: HashMap::new(),
546            authenticated: true,
547            x25519_key: None,
548            p2p_addr: None,
549        };
550        assert!(AuthenticatedValidator::try_from(&v).is_err());
551        assert!(AuthenticatedValidator::try_from(v).is_err());
552    }
553
554    #[test]
555    fn test_commit_none_vs_some_differs() {
556        let account = Address::random();
557        let state_ver_key = Some(StateVerKey::default());
558        let stake = U256::from(7u64);
559
560        let with_key: RegisteredValidator<BLSPubKey> = RegisteredValidator {
561            account,
562            stake_table_key: Some(BLSPubKey::generated_from_seed_indexed([2u8; 32], 0).0),
563            state_ver_key: state_ver_key.clone(),
564            stake,
565            commission: 100,
566            delegators: HashMap::new(),
567            authenticated: false,
568            x25519_key: None,
569            p2p_addr: None,
570        };
571        let without_key: RegisteredValidator<BLSPubKey> = RegisteredValidator {
572            account,
573            stake_table_key: None,
574            state_ver_key,
575            stake,
576            commission: 100,
577            delegators: HashMap::new(),
578            authenticated: false,
579            x25519_key: None,
580            p2p_addr: None,
581        };
582        assert_ne!(
583            with_key.commit().as_ref() as &[u8],
584            without_key.commit().as_ref() as &[u8]
585        );
586    }
587
588    #[test]
589    fn test_commit_none_vs_some_schnorr_differs() {
590        let account = Address::random();
591        let stake_table_key = Some(BLSPubKey::generated_from_seed_indexed([3u8; 32], 0).0);
592        let stake = U256::from(11u64);
593
594        let with_schnorr: RegisteredValidator<BLSPubKey> = RegisteredValidator {
595            account,
596            stake_table_key,
597            state_ver_key: Some(StateVerKey::default()),
598            stake,
599            commission: 100,
600            delegators: HashMap::new(),
601            authenticated: false,
602            x25519_key: None,
603            p2p_addr: None,
604        };
605        let without_schnorr: RegisteredValidator<BLSPubKey> = RegisteredValidator {
606            account,
607            stake_table_key,
608            state_ver_key: None,
609            stake,
610            commission: 100,
611            delegators: HashMap::new(),
612            authenticated: false,
613            x25519_key: None,
614            p2p_addr: None,
615        };
616        assert_ne!(
617            with_schnorr.commit().as_ref() as &[u8],
618            without_schnorr.commit().as_ref() as &[u8]
619        );
620    }
621}