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::types::SignatureKey;
11use hotshot_contract_adapter::sol_types::StakeTableV3::{
12    CommissionUpdated, ConsensusKeysUpdated, ConsensusKeysUpdatedV2, Delegated, P2pAddrUpdated,
13    Undelegated, UndelegatedV2, ValidatorExit, ValidatorExitV2, ValidatorRegistered,
14    ValidatorRegisteredV2, ValidatorRegisteredV3, X25519KeyUpdated,
15};
16use hotshot_types::{
17    PeerConfig, addr::NetAddr, data::EpochNumber, light_client::StateVerKey,
18    network::PeerConfigKeys, x25519,
19};
20use itertools::Itertools;
21use jf_utils::to_bytes;
22use serde::{Deserialize, Serialize};
23use thiserror::Error;
24use tokio::task::JoinHandle;
25use vbs::version::Version;
26use versions::NEW_PROTOCOL_VERSION;
27
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/// May or may not have valid signatures (contract can't fully verify Schnorr).
54/// Used for state tracking. To participate in consensus, must be authenticated
55/// and converted to `AuthenticatedValidator` via `TryFrom`.
56#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
57#[serde(bound(deserialize = ""))]
58pub struct RegisteredValidator<KEY: SignatureKey> {
59    pub account: Address,
60    /// The peer's public key
61    pub stake_table_key: KEY,
62    /// the peer's state public key
63    pub state_ver_key: StateVerKey,
64    /// the peer's stake
65    pub stake: U256,
66    // commission
67    // TODO: MA commission is only valid from 0 to 10_000. Add newtype to enforce this.
68    pub commission: u16,
69    pub delegators: HashMap<Address, U256>,
70    /// Whether the validator's registration signature has been verified.
71    /// Contract can verify BLS but only length-check Schnorr.
72    pub authenticated: bool,
73    /// Public X25519 key for network communication.
74    pub x25519_key: Option<x25519::PublicKey>,
75    /// Network address.
76    pub p2p_addr: Option<NetAddr>,
77}
78
79/// Validator eligible for consensus participation.
80/// Guaranteed to have valid BLS and Schnorr signatures.
81/// This is a newtype wrapper around RegisteredValidator that guarantees authenticated=true.
82#[derive(serde::Serialize, Clone, Debug, PartialEq, Eq)]
83pub struct AuthenticatedValidator<KEY: SignatureKey>(RegisteredValidator<KEY>);
84
85impl<'de, KEY: SignatureKey> Deserialize<'de> for AuthenticatedValidator<KEY> {
86    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
87    where
88        D: serde::Deserializer<'de>,
89    {
90        let inner = RegisteredValidator::deserialize(deserializer)?;
91        if !inner.authenticated {
92            return Err(serde::de::Error::custom(
93                "cannot deserialize unauthenticated validator as AuthenticatedValidator",
94            ));
95        }
96        Ok(AuthenticatedValidator(inner))
97    }
98}
99
100impl<KEY: SignatureKey> AuthenticatedValidator<KEY> {
101    pub fn into_inner(self) -> RegisteredValidator<KEY> {
102        self.0
103    }
104
105    /// Whether this validator can participate in consensus at `protocol_version`.
106    ///
107    /// Encodes only protocol-version-gated requirements; stake and delegation checks
108    /// live in `select_active_validator_set`.
109    ///
110    /// # Liveness during the CLIQUENET upgrade transition
111    ///
112    /// The active validator set for epoch N is selected at the epoch root header in
113    /// epoch N-2, using that root header's protocol version (see `Fetcher::fetch`).
114    /// When CLIQUENET activates at some epoch K, the roots for epochs K and K+1 are
115    /// pre-CLIQUENET headers, so those active sets were selected without this filter
116    /// and may include validators missing `x25519_key` or `p2p_addr`.
117    ///
118    /// In epochs K and K+1, the cliquenet network silently skips peers without
119    /// connect info (no panic, no error), but they still count toward the quorum
120    /// threshold. Liveness through the transition therefore requires the eligible
121    /// subset of validators to hold >= 2/3 of total active stake. From epoch K+2
122    /// onward, roots are post-CLIQUENET and this filter applies.
123    ///
124    /// Operators are warned at startup on the version immediately preceding
125    /// NEW_PROTOCOL if their on-chain entry is missing network info.
126    pub fn is_eligible(&self, protocol_version: Version) -> bool {
127        if protocol_version >= NEW_PROTOCOL_VERSION
128            && (self.x25519_key.is_none() || self.p2p_addr.is_none())
129        {
130            return false;
131        }
132        true
133    }
134}
135
136impl<KEY: SignatureKey> std::ops::Deref for AuthenticatedValidator<KEY> {
137    type Target = RegisteredValidator<KEY>;
138
139    fn deref(&self) -> &Self::Target {
140        &self.0
141    }
142}
143
144#[derive(Debug, Error)]
145#[error("Validator {0:#x} not authenticated (invalid registration signature)")]
146pub struct UnauthenticatedValidatorError(pub Address);
147
148impl<KEY: SignatureKey + Clone> TryFrom<&RegisteredValidator<KEY>> for AuthenticatedValidator<KEY> {
149    type Error = UnauthenticatedValidatorError;
150
151    fn try_from(v: &RegisteredValidator<KEY>) -> Result<Self, Self::Error> {
152        if !v.authenticated {
153            return Err(UnauthenticatedValidatorError(v.account));
154        }
155        Ok(AuthenticatedValidator(v.clone()))
156    }
157}
158
159impl<KEY: SignatureKey> TryFrom<RegisteredValidator<KEY>> for AuthenticatedValidator<KEY> {
160    type Error = UnauthenticatedValidatorError;
161
162    fn try_from(v: RegisteredValidator<KEY>) -> Result<Self, Self::Error> {
163        if !v.authenticated {
164            return Err(UnauthenticatedValidatorError(v.account));
165        }
166        Ok(AuthenticatedValidator(v))
167    }
168}
169
170impl<KEY: SignatureKey> From<AuthenticatedValidator<KEY>> for RegisteredValidator<KEY> {
171    fn from(v: AuthenticatedValidator<KEY>) -> Self {
172        v.into_inner()
173    }
174}
175
176impl<KEY: SignatureKey> Committable for RegisteredValidator<KEY> {
177    fn commit(&self) -> Commitment<Self> {
178        let mut builder = RawCommitmentBuilder::new(&Self::tag())
179            .fixed_size_field("account", &self.account)
180            .var_size_field(
181                "stake_table_key",
182                self.stake_table_key.to_bytes().as_slice(),
183            )
184            .var_size_field("state_ver_key", &to_bytes!(&self.state_ver_key).unwrap())
185            .fixed_size_field("stake", &to_fixed_bytes(self.stake))
186            .constant_str("commission")
187            .u16(self.commission);
188
189        // x25519_key and p2p_addr are included in the commitment only when set.
190        // They are None until StakeTableV3 is deployed and the validator sets them.
191        // This maintains backwards compatibility with pre-V3 commitments.
192        if let Some(key) = &self.x25519_key {
193            builder = builder.var_size_field("x25519_key", key.as_slice());
194        }
195        if let Some(addr) = &self.p2p_addr {
196            builder = builder.var_size_field("p2p_addr", addr.to_string().as_bytes());
197        }
198
199        builder = builder.constant_str("delegators");
200        for (address, stake) in self.delegators.iter().sorted() {
201            builder = builder
202                .fixed_size_bytes(address)
203                .fixed_size_bytes(&to_fixed_bytes(*stake));
204        }
205
206        // Backwards compatibility: don't change the commitment of *authenticated* validators
207        if !self.authenticated {
208            builder = builder.constant_str("unauthenticated");
209        }
210
211        builder.finalize()
212    }
213
214    fn tag() -> String {
215        "VALIDATOR".to_string()
216    }
217}
218
219#[derive(serde::Serialize, serde::Deserialize, std::hash::Hash, Clone, Debug, PartialEq, Eq)]
220#[serde(bound(deserialize = ""))]
221pub struct Delegator {
222    pub address: Address,
223    pub validator: Address,
224    pub stake: U256,
225}
226
227/// Type for holding result sets matching epochs to stake tables.
228pub type IndexedStake = (
229    EpochNumber,
230    (AuthenticatedValidatorMap, Option<RewardAmount>),
231    Option<StakeTableHash>,
232);
233
234#[derive(Clone, derive_more::derive::Debug)]
235pub struct Fetcher {
236    /// Peers for catching up the stake table
237    #[debug(skip)]
238    pub(crate) peers: Arc<dyn StateCatchup>,
239    /// Methods for stake table persistence.
240    #[debug(skip)]
241    pub(crate) persistence: Arc<Mutex<dyn MembershipPersistence>>,
242    /// L1 provider
243    pub(crate) l1_client: L1Client,
244    /// Verifiable `ChainConfig` holding contract address
245    pub(crate) chain_config: Arc<Mutex<ChainConfig>>,
246    pub(crate) update_task: Arc<StakeTableUpdateTask>,
247    pub initial_supply: Arc<RwLock<Option<U256>>>,
248}
249
250#[derive(Debug, Default)]
251pub(crate) struct StakeTableUpdateTask(pub(crate) Mutex<Option<JoinHandle<()>>>);
252
253impl Drop for StakeTableUpdateTask {
254    fn drop(&mut self) {
255        if let Some(task) = self.0.get_mut().take() {
256            task.abort();
257        }
258    }
259}
260
261// (log block number, log index)
262pub type EventKey = (u64, u64);
263
264#[derive(Clone, derive_more::From, PartialEq, serde::Serialize, serde::Deserialize)]
265pub enum StakeTableEvent {
266    Register(ValidatorRegistered),
267    RegisterV2(ValidatorRegisteredV2),
268    Deregister(ValidatorExit),
269    DeregisterV2(ValidatorExitV2),
270    Delegate(Delegated),
271    Undelegate(Undelegated),
272    UndelegateV2(UndelegatedV2),
273    KeyUpdate(ConsensusKeysUpdated),
274    KeyUpdateV2(ConsensusKeysUpdatedV2),
275    CommissionUpdate(CommissionUpdated),
276    RegisterV3(ValidatorRegisteredV3),
277    X25519KeyUpdate(X25519KeyUpdated),
278    P2pAddrUpdate(P2pAddrUpdated),
279}
280
281#[derive(Debug, Error)]
282pub enum StakeTableError {
283    #[error("Validator {0:#x} already registered")]
284    AlreadyRegistered(Address),
285    #[error("Validator {0:#x} not found")]
286    ValidatorNotFound(Address),
287    #[error("Delegator {0:#x} not found")]
288    DelegatorNotFound(Address),
289    #[error("BLS key already used: {0}")]
290    BlsKeyAlreadyUsed(String),
291    #[error("Insufficient stake to undelegate")]
292    InsufficientStake,
293    #[error("Event authentication failed: {0}")]
294    AuthenticationFailed(String),
295    #[error("No validators met the minimum criteria (non-zero stake and at least one delegator)")]
296    NoValidValidators,
297    #[error("Could not compute maximum stake from filtered validators")]
298    MissingMaximumStake,
299    #[error("Overflow when calculating minimum stake threshold")]
300    MinimumStakeOverflow,
301    #[error("Delegator {0:#x} has 0 stake")]
302    ZeroDelegatorStake(Address),
303    #[error("Failed to hash stake table: {0}")]
304    HashError(#[from] bincode::Error),
305    #[error("Validator {0:#x} already exited and cannot be re-registered")]
306    ValidatorAlreadyExited(Address),
307    #[error("Validator {0:#x} has invalid commission {1}")]
308    InvalidCommission(Address, u16),
309    #[error("Schnorr key already used: {0}")]
310    SchnorrKeyAlreadyUsed(String),
311    #[error("x25519 key already used: {0}")]
312    X25519KeyAlreadyUsed(String),
313    #[error("Invalid x25519 key: {0}")]
314    InvalidX25519Key(String),
315    #[error("Stake table event decode error {0}")]
316    StakeTableEventDecodeError(#[from] alloy::sol_types::Error),
317    #[error("Stake table events sorting error: {0}")]
318    EventSortingError(#[from] EventSortingError),
319}
320
321#[derive(Debug, Error)]
322pub enum ExpectedStakeTableError {
323    #[error("Schnorr key already used: {0}")]
324    SchnorrKeyAlreadyUsed(String),
325}
326
327#[derive(Debug, Error)]
328pub enum FetchRewardError {
329    #[error("No stake table contract address found in chain config")]
330    MissingStakeTableContract,
331
332    #[error("Token address fetch failed: {0}")]
333    TokenAddressFetch(#[source] alloy::contract::Error),
334
335    #[error("Token Initialized event logs are empty")]
336    MissingInitializedEvent,
337
338    #[error("Transaction hash not found in Initialized event log: {init_log:?}")]
339    MissingTransactionHash { init_log: Log },
340
341    #[error("Block number not found in Initialized event log")]
342    MissingBlockNumber,
343
344    #[error("Transfer event query failed: {0}")]
345    TransferEventQuery(#[source] alloy::contract::Error),
346
347    #[error("No Transfer event found in the Initialized event block")]
348    MissingTransferEvent,
349
350    #[error("Division by zero {0}")]
351    DivisionByZero(&'static str),
352
353    #[error("Overflow {0}")]
354    Overflow(&'static str),
355
356    #[error("Contract call failed: {0}")]
357    ContractCall(#[source] alloy::contract::Error),
358
359    #[error("Rpc call failed: {0}")]
360    Rpc(#[source] RpcError<TransportErrorKind>),
361
362    #[error("Exceeded max block range scan ({0} blocks) while searching for Initialized event")]
363    ExceededMaxScanRange(u64),
364
365    #[error("Scanning for Initialized event failed: {0}")]
366    ScanQueryFailed(#[source] alloy::contract::Error),
367}
368
369#[derive(Debug, thiserror::Error)]
370pub enum EventSortingError {
371    #[error("Missing block number in log")]
372    MissingBlockNumber,
373
374    #[error("Missing log index in log")]
375    MissingLogIndex,
376
377    #[error("Invalid stake table event")]
378    InvalidStakeTableEvent,
379}
380
381#[cfg(test)]
382mod tests {
383    use std::collections::HashMap;
384
385    use alloy::primitives::{Address, U256};
386    use committable::Committable;
387    use hotshot::types::{BLSPubKey, SignatureKey};
388    use hotshot_types::{addr::NetAddr, light_client::StateVerKey, x25519};
389
390    use super::RegisteredValidator;
391
392    /// Both x25519_key and p2p_addr must independently affect the commitment.
393    #[test]
394    fn test_commitment_changes_with_x25519_and_p2p_fields() {
395        let base = RegisteredValidator::<BLSPubKey>::mock();
396        assert!(base.x25519_key.is_none());
397        assert!(base.p2p_addr.is_none());
398        let commit_base = base.commit();
399
400        let mut with_x25519 = base.clone();
401        with_x25519.x25519_key = Some(x25519::PublicKey::try_from([42u8; 32].as_slice()).unwrap());
402        let commit_x25519 = with_x25519.commit();
403
404        let mut with_p2p = base.clone();
405        with_p2p.p2p_addr = Some("127.0.0.1:8080".parse::<NetAddr>().unwrap());
406        let commit_p2p = with_p2p.commit();
407
408        assert_ne!(commit_base, commit_x25519);
409        assert_ne!(commit_base, commit_p2p);
410        assert_ne!(commit_x25519, commit_p2p);
411    }
412
413    /// Unauthenticated validators must produce a different commitment than authenticated ones.
414    /// This ensures validators with invalid signatures are distinguishable in the commitment tree.
415    #[test]
416    fn test_unauthenticated_validator_commitment_differs() {
417        let account = Address::random();
418        let stake_table_key = BLSPubKey::generated_from_seed_indexed([1u8; 32], 0).0;
419        let state_ver_key = StateVerKey::default();
420        let stake = U256::from(1000);
421        let commission = 500u16;
422        let delegators = HashMap::new();
423
424        let authenticated = RegisteredValidator {
425            account,
426            stake_table_key,
427            state_ver_key: state_ver_key.clone(),
428            stake,
429            commission,
430            delegators: delegators.clone(),
431            authenticated: true,
432            x25519_key: None,
433            p2p_addr: None,
434        };
435
436        let unauthenticated = RegisteredValidator {
437            account,
438            stake_table_key,
439            state_ver_key,
440            stake,
441            commission,
442            delegators,
443            authenticated: false,
444            x25519_key: None,
445            p2p_addr: None,
446        };
447
448        let auth_commitment = authenticated.commit();
449        let unauth_commitment = unauthenticated.commit();
450        assert_ne!(
451            auth_commitment.as_ref() as &[u8],
452            unauth_commitment.as_ref() as &[u8]
453        );
454    }
455}