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::StakeTableV2::{
12    CommissionUpdated, ConsensusKeysUpdated, ConsensusKeysUpdatedV2, Delegated, Undelegated,
13    UndelegatedV2, ValidatorExit, ValidatorExitV2, ValidatorRegistered, ValidatorRegisteredV2,
14};
15use hotshot_types::{
16    PeerConfig, data::EpochNumber, light_client::StateVerKey, network::PeerConfigKeys, x25519,
17    addr::NetAddr
18};
19use itertools::Itertools;
20use jf_utils::to_bytes;
21use serde::{Deserialize, Serialize};
22use thiserror::Error;
23use tokio::task::JoinHandle;
24
25use super::L1Client;
26use crate::{
27    traits::{MembershipPersistence, StateCatchup},
28    v0::{impls::StakeTableHash, ChainConfig},
29    v0_3::RewardAmount,
30    AuthenticatedValidatorMap, SeqTypes,
31};
32/// Stake table holding all staking information (DA and non-DA stakers)
33#[derive(Debug, Clone, Serialize, Deserialize, From)]
34pub struct CombinedStakeTable(Vec<PeerConfigKeys<SeqTypes>>);
35
36#[derive(Clone, Debug, From, Into, Serialize, Deserialize, PartialEq, Eq)]
37/// NewType to disambiguate DA Membership
38pub struct DAMembers(pub Vec<PeerConfig<SeqTypes>>);
39
40#[derive(Clone, Debug, From, Into, Serialize, Deserialize, PartialEq, Eq)]
41/// NewType to disambiguate StakeTable
42pub struct StakeTable(pub Vec<PeerConfig<SeqTypes>>);
43
44pub(crate) fn to_fixed_bytes(value: U256) -> [u8; std::mem::size_of::<U256>()] {
45    let bytes: [u8; std::mem::size_of::<U256>()] = value.to_le_bytes();
46    bytes
47}
48
49/// Validator as registered in the stake table contract.
50/// May or may not have valid signatures (contract can't fully verify Schnorr).
51/// Used for state tracking. To participate in consensus, must be authenticated
52/// and converted to `AuthenticatedValidator` via `TryFrom`.
53#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)]
54#[serde(bound(deserialize = ""))]
55pub struct RegisteredValidator<KEY: SignatureKey> {
56    pub account: Address,
57    /// The peer's public key
58    pub stake_table_key: KEY,
59    /// the peer's state public key
60    pub state_ver_key: StateVerKey,
61    /// the peer's stake
62    pub stake: U256,
63    // commission
64    // TODO: MA commission is only valid from 0 to 10_000. Add newtype to enforce this.
65    pub commission: u16,
66    pub delegators: HashMap<Address, U256>,
67    /// Whether the validator's registration signature has been verified.
68    /// Contract can verify BLS but only length-check Schnorr.
69    pub authenticated: bool,
70    /// Public X25519 key for network communication.
71    pub x25519_key: Option<x25519::PublicKey>,
72    /// Network address.
73    pub p2p_addr: Option<NetAddr>
74}
75
76/// Validator eligible for consensus participation.
77/// Guaranteed to have valid BLS and Schnorr signatures.
78/// This is a newtype wrapper around RegisteredValidator that guarantees authenticated=true.
79#[derive(serde::Serialize, Clone, Debug, PartialEq, Eq)]
80pub struct AuthenticatedValidator<KEY: SignatureKey>(RegisteredValidator<KEY>);
81
82impl<'de, KEY: SignatureKey> Deserialize<'de> for AuthenticatedValidator<KEY> {
83    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84    where
85        D: serde::Deserializer<'de>,
86    {
87        let inner = RegisteredValidator::deserialize(deserializer)?;
88        if !inner.authenticated {
89            return Err(serde::de::Error::custom(
90                "cannot deserialize unauthenticated validator as AuthenticatedValidator",
91            ));
92        }
93        Ok(AuthenticatedValidator(inner))
94    }
95}
96
97impl<KEY: SignatureKey> AuthenticatedValidator<KEY> {
98    pub fn into_inner(self) -> RegisteredValidator<KEY> {
99        self.0
100    }
101}
102
103impl<KEY: SignatureKey> std::ops::Deref for AuthenticatedValidator<KEY> {
104    type Target = RegisteredValidator<KEY>;
105
106    fn deref(&self) -> &Self::Target {
107        &self.0
108    }
109}
110
111#[derive(Debug, Error)]
112#[error("Validator {0:#x} not authenticated (invalid registration signature)")]
113pub struct UnauthenticatedValidatorError(pub Address);
114
115impl<KEY: SignatureKey + Clone> TryFrom<&RegisteredValidator<KEY>> for AuthenticatedValidator<KEY> {
116    type Error = UnauthenticatedValidatorError;
117
118    fn try_from(v: &RegisteredValidator<KEY>) -> Result<Self, Self::Error> {
119        if !v.authenticated {
120            return Err(UnauthenticatedValidatorError(v.account));
121        }
122        Ok(AuthenticatedValidator(v.clone()))
123    }
124}
125
126impl<KEY: SignatureKey> TryFrom<RegisteredValidator<KEY>> for AuthenticatedValidator<KEY> {
127    type Error = UnauthenticatedValidatorError;
128
129    fn try_from(v: RegisteredValidator<KEY>) -> Result<Self, Self::Error> {
130        if !v.authenticated {
131            return Err(UnauthenticatedValidatorError(v.account));
132        }
133        Ok(AuthenticatedValidator(v))
134    }
135}
136
137impl<KEY: SignatureKey> From<AuthenticatedValidator<KEY>> for RegisteredValidator<KEY> {
138    fn from(v: AuthenticatedValidator<KEY>) -> Self {
139        v.into_inner()
140    }
141}
142
143impl<KEY: SignatureKey> Committable for RegisteredValidator<KEY> {
144    fn commit(&self) -> Commitment<Self> {
145        let mut builder = RawCommitmentBuilder::new(&Self::tag())
146            .fixed_size_field("account", &self.account)
147            .var_size_field(
148                "stake_table_key",
149                self.stake_table_key.to_bytes().as_slice(),
150            )
151            .var_size_field("state_ver_key", &to_bytes!(&self.state_ver_key).unwrap())
152            .fixed_size_field("stake", &to_fixed_bytes(self.stake))
153            .constant_str("commission")
154            .u16(self.commission);
155            //.var_size_field("x25519_key", self.x25519_key.as_ref().map(|k| k.as_slice()).unwrap_or_default())
156            //.var_size_field("p2p_addr", self.p2p_addr.as_ref().map(|a| a.to_string()).unwrap_or_default().as_bytes());
157
158        builder = builder.constant_str("delegators");
159        for (address, stake) in self.delegators.iter().sorted() {
160            builder = builder
161                .fixed_size_bytes(address)
162                .fixed_size_bytes(&to_fixed_bytes(*stake));
163        }
164
165        // Backwards compatibility: don't change the commitment of *authenticated* validators
166        if !self.authenticated {
167            builder = builder.constant_str("unauthenticated");
168        }
169
170        builder.finalize()
171    }
172
173    fn tag() -> String {
174        "VALIDATOR".to_string()
175    }
176}
177
178#[derive(serde::Serialize, serde::Deserialize, std::hash::Hash, Clone, Debug, PartialEq, Eq)]
179#[serde(bound(deserialize = ""))]
180pub struct Delegator {
181    pub address: Address,
182    pub validator: Address,
183    pub stake: U256,
184}
185
186/// Type for holding result sets matching epochs to stake tables.
187pub type IndexedStake = (
188    EpochNumber,
189    (AuthenticatedValidatorMap, Option<RewardAmount>),
190    Option<StakeTableHash>,
191);
192
193#[derive(Clone, derive_more::derive::Debug)]
194pub struct Fetcher {
195    /// Peers for catching up the stake table
196    #[debug(skip)]
197    pub(crate) peers: Arc<dyn StateCatchup>,
198    /// Methods for stake table persistence.
199    #[debug(skip)]
200    pub(crate) persistence: Arc<Mutex<dyn MembershipPersistence>>,
201    /// L1 provider
202    pub(crate) l1_client: L1Client,
203    /// Verifiable `ChainConfig` holding contract address
204    pub(crate) chain_config: Arc<Mutex<ChainConfig>>,
205    pub(crate) update_task: Arc<StakeTableUpdateTask>,
206    pub initial_supply: Arc<RwLock<Option<U256>>>,
207}
208
209#[derive(Debug, Default)]
210pub(crate) struct StakeTableUpdateTask(pub(crate) Mutex<Option<JoinHandle<()>>>);
211
212impl Drop for StakeTableUpdateTask {
213    fn drop(&mut self) {
214        if let Some(task) = self.0.get_mut().take() {
215            task.abort();
216        }
217    }
218}
219
220// (log block number, log index)
221pub type EventKey = (u64, u64);
222
223#[derive(Clone, derive_more::From, PartialEq, serde::Serialize, serde::Deserialize)]
224pub enum StakeTableEvent {
225    Register(ValidatorRegistered),
226    RegisterV2(ValidatorRegisteredV2),
227    Deregister(ValidatorExit),
228    DeregisterV2(ValidatorExitV2),
229    Delegate(Delegated),
230    Undelegate(Undelegated),
231    UndelegateV2(UndelegatedV2),
232    KeyUpdate(ConsensusKeysUpdated),
233    KeyUpdateV2(ConsensusKeysUpdatedV2),
234    CommissionUpdate(CommissionUpdated),
235}
236
237#[derive(Debug, Error)]
238pub enum StakeTableError {
239    #[error("Validator {0:#x} already registered")]
240    AlreadyRegistered(Address),
241    #[error("Validator {0:#x} not found")]
242    ValidatorNotFound(Address),
243    #[error("Delegator {0:#x} not found")]
244    DelegatorNotFound(Address),
245    #[error("BLS key already used: {0}")]
246    BlsKeyAlreadyUsed(String),
247    #[error("Insufficient stake to undelegate")]
248    InsufficientStake,
249    #[error("Event authentication failed: {0}")]
250    AuthenticationFailed(String),
251    #[error("No validators met the minimum criteria (non-zero stake and at least one delegator)")]
252    NoValidValidators,
253    #[error("Could not compute maximum stake from filtered validators")]
254    MissingMaximumStake,
255    #[error("Overflow when calculating minimum stake threshold")]
256    MinimumStakeOverflow,
257    #[error("Delegator {0:#x} has 0 stake")]
258    ZeroDelegatorStake(Address),
259    #[error("Failed to hash stake table: {0}")]
260    HashError(#[from] bincode::Error),
261    #[error("Validator {0:#x} already exited and cannot be re-registered")]
262    ValidatorAlreadyExited(Address),
263    #[error("Validator {0:#x} has invalid commission {1}")]
264    InvalidCommission(Address, u16),
265    #[error("Schnorr key already used: {0}")]
266    SchnorrKeyAlreadyUsed(String),
267    #[error("Stake table event decode error {0}")]
268    StakeTableEventDecodeError(#[from] alloy::sol_types::Error),
269    #[error("Stake table events sorting error: {0}")]
270    EventSortingError(#[from] EventSortingError),
271}
272
273#[derive(Debug, Error)]
274pub enum ExpectedStakeTableError {
275    #[error("Schnorr key already used: {0}")]
276    SchnorrKeyAlreadyUsed(String),
277}
278
279#[derive(Debug, Error)]
280pub enum FetchRewardError {
281    #[error("No stake table contract address found in chain config")]
282    MissingStakeTableContract,
283
284    #[error("Token address fetch failed: {0}")]
285    TokenAddressFetch(#[source] alloy::contract::Error),
286
287    #[error("Token Initialized event logs are empty")]
288    MissingInitializedEvent,
289
290    #[error("Transaction hash not found in Initialized event log: {init_log:?}")]
291    MissingTransactionHash { init_log: Log },
292
293    #[error("Block number not found in Initialized event log")]
294    MissingBlockNumber,
295
296    #[error("Transfer event query failed: {0}")]
297    TransferEventQuery(#[source] alloy::contract::Error),
298
299    #[error("No Transfer event found in the Initialized event block")]
300    MissingTransferEvent,
301
302    #[error("Division by zero {0}")]
303    DivisionByZero(&'static str),
304
305    #[error("Overflow {0}")]
306    Overflow(&'static str),
307
308    #[error("Contract call failed: {0}")]
309    ContractCall(#[source] alloy::contract::Error),
310
311    #[error("Rpc call failed: {0}")]
312    Rpc(#[source] RpcError<TransportErrorKind>),
313
314    #[error("Exceeded max block range scan ({0} blocks) while searching for Initialized event")]
315    ExceededMaxScanRange(u64),
316
317    #[error("Scanning for Initialized event failed: {0}")]
318    ScanQueryFailed(#[source] alloy::contract::Error),
319}
320
321#[derive(Debug, thiserror::Error)]
322pub enum EventSortingError {
323    #[error("Missing block number in log")]
324    MissingBlockNumber,
325
326    #[error("Missing log index in log")]
327    MissingLogIndex,
328
329    #[error("Invalid stake table V2 event")]
330    InvalidStakeTableV2Event,
331}
332
333#[cfg(test)]
334mod tests {
335    use std::collections::HashMap;
336
337    use alloy::primitives::{Address, U256};
338    use committable::Committable;
339    use hotshot::types::{BLSPubKey, SignatureKey};
340    use hotshot_types::light_client::StateVerKey;
341
342    use super::RegisteredValidator;
343
344    /// Unauthenticated validators must produce a different commitment than authenticated ones.
345    /// This ensures validators with invalid signatures are distinguishable in the commitment tree.
346    #[test]
347    fn test_unauthenticated_validator_commitment_differs() {
348        let account = Address::random();
349        let stake_table_key = BLSPubKey::generated_from_seed_indexed([1u8; 32], 0).0;
350        let state_ver_key = StateVerKey::default();
351        let stake = U256::from(1000);
352        let commission = 500u16;
353        let delegators = HashMap::new();
354
355        let authenticated = RegisteredValidator {
356            account,
357            stake_table_key,
358            state_ver_key: state_ver_key.clone(),
359            stake,
360            commission,
361            delegators: delegators.clone(),
362            authenticated: true,
363            x25519_key: None,
364            p2p_addr: None,
365        };
366
367        let unauthenticated = RegisteredValidator {
368            account,
369            stake_table_key,
370            state_ver_key,
371            stake,
372            commission,
373            delegators,
374            authenticated: false,
375            x25519_key: None,
376            p2p_addr: None,
377        };
378
379        let auth_commitment = authenticated.commit();
380        let unauth_commitment = unauthenticated.commit();
381        assert_ne!(
382            auth_commitment.as_ref() as &[u8],
383            unauth_commitment.as_ref() as &[u8]
384        );
385    }
386}