Skip to main content

espresso_node/state_signature/relay_server/
lcv3_relay.rs

1use std::{
2    collections::{BTreeSet, HashMap, hash_map::Entry},
3    sync::Arc,
4};
5
6use alloy::primitives::U256;
7use axum::http::StatusCode;
8use hotshot_contract_adapter::light_client::derive_signed_state_digest;
9use hotshot_types::{
10    light_client::{
11        LCV3StateSignatureRequestBody, LCV3StateSignaturesBundle, LightClientState, StateVerKey,
12    },
13    traits::signature_key::LCV3StateSignatureKey,
14};
15
16use super::{RelayError, stake_table_tracker::StakeTableTracker};
17
18#[async_trait::async_trait]
19pub trait LCV3StateRelayServerDataSource {
20    /// Get the latest available signatures bundle.
21    /// # Errors
22    /// Errors if there's no available signatures bundle.
23    fn get_latest_signature_bundle(&self) -> Result<LCV3StateSignaturesBundle, RelayError>;
24
25    /// Post a signature to the relay server
26    /// # Errors
27    /// Errors if the signature is invalid, already posted, or no longer needed.
28    async fn post_signature(
29        &mut self,
30        req: LCV3StateSignatureRequestBody,
31    ) -> Result<(), RelayError>;
32}
33
34/// Server state that tracks the light client V3 state and signatures
35pub struct LCV3StateRelayServerState {
36    /// Bundles for light client V3
37    bundles: HashMap<u64, HashMap<LightClientState, LCV3StateSignaturesBundle>>,
38
39    /// The latest state signatures bundle for LCV3 light client
40    latest_available_bundle: Option<LCV3StateSignaturesBundle>,
41    /// The block height of the latest available LCV3 state signature bundle
42    latest_block_height: Option<u64>,
43
44    /// A ordered queue of block heights for V3 light client state, used for garbage collection.
45    gc_queue: BTreeSet<u64>,
46
47    /// Stake table tracker
48    stake_table_tracker: Arc<StakeTableTracker>,
49}
50
51#[async_trait::async_trait]
52impl LCV3StateRelayServerDataSource for LCV3StateRelayServerState {
53    fn get_latest_signature_bundle(&self) -> Result<LCV3StateSignaturesBundle, RelayError> {
54        self.latest_available_bundle
55            .clone()
56            .ok_or(RelayError::catch_all(
57                StatusCode::NOT_FOUND,
58                "The light client V3 state signatures are not ready.".to_owned(),
59            ))
60    }
61
62    async fn post_signature(
63        &mut self,
64        req: LCV3StateSignatureRequestBody,
65    ) -> Result<(), RelayError> {
66        let block_height = req.state.block_height;
67        if block_height <= self.latest_block_height.unwrap_or(0) {
68            // This signature is no longer needed
69            return Ok(());
70        }
71        let stake_table = self
72            .stake_table_tracker
73            .stake_table_info_for_block(block_height)
74            .await
75            .map_err(|e| RelayError::catch_all(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
76        let Some(weight) = stake_table.known_nodes.get(&req.key) else {
77            tracing::warn!("Received LCV3 signature from unknown node: {req}");
78            return Err(RelayError::catch_all(
79                StatusCode::UNAUTHORIZED,
80                "LCV3 signature posted by nodes not on the stake table".to_owned(),
81            ));
82        };
83
84        // sanity check the signature validity first before adding in
85        let signed_state_digest =
86            derive_signed_state_digest(&req.state, &req.next_stake, &req.auth_root);
87        if !<StateVerKey as LCV3StateSignatureKey>::verify_state_sig(
88            &req.key,
89            &req.signature,
90            signed_state_digest,
91        ) {
92            tracing::warn!("Couldn't verify the received LCV3 signature: {req}");
93            return Err(RelayError::catch_all(
94                StatusCode::BAD_REQUEST,
95                "The posted LCV3 signature is not valid.".to_owned(),
96            ));
97        }
98
99        let bundles_at_height = self.bundles.entry(block_height).or_default();
100        self.gc_queue.insert(block_height);
101
102        let bundle = bundles_at_height
103            .entry(req.state)
104            .or_insert(LCV3StateSignaturesBundle {
105                state: req.state,
106                next_stake: req.next_stake,
107                auth_root: req.auth_root,
108                signatures: Default::default(),
109                accumulated_weight: U256::from(0),
110            });
111        tracing::debug!(
112            "Accepting new LCV3 signature for block height {} from {}.",
113            block_height,
114            req.key
115        );
116        match bundle.signatures.entry(req.key) {
117            Entry::Occupied(_) => {
118                // A signature is already posted for this key with this state
119                return Err(RelayError::catch_all(
120                    StatusCode::BAD_REQUEST,
121                    "A LCV3 signature of this light client state is already posted at this block \
122                     height for this key."
123                        .to_owned(),
124                ));
125            },
126            Entry::Vacant(entry) => {
127                entry.insert(req.signature);
128                bundle.accumulated_weight += *weight;
129            },
130        }
131
132        if bundle.accumulated_weight >= stake_table.threshold {
133            tracing::info!(
134                "Light client V3 state signature bundle at block height {} is ready to serve.",
135                block_height
136            );
137            self.latest_block_height = Some(block_height);
138            self.latest_available_bundle = Some(bundle.clone());
139
140            // garbage collect
141            self.prune(block_height);
142        }
143
144        Ok(())
145    }
146}
147
148impl LCV3StateRelayServerState {
149    /// Centralizing all garbage-collection logic, won't panic, won't error, simply do nothing if nothing to prune.
150    /// `until_height` is inclusive, meaning that would also be pruned.
151    pub fn prune(&mut self, until_height: u64) {
152        while let Some(&height) = self.gc_queue.first() {
153            if height > until_height {
154                return;
155            }
156            self.bundles.remove(&height);
157            self.gc_queue.pop_first();
158            tracing::debug!(%height, "garbage collected for ");
159        }
160    }
161
162    pub fn new(stake_table_tracker: Arc<StakeTableTracker>) -> Self {
163        Self {
164            bundles: HashMap::new(),
165            latest_available_bundle: None,
166            latest_block_height: None,
167            gc_queue: BTreeSet::new(),
168            stake_table_tracker,
169        }
170    }
171}