Skip to main content

espresso_node/state_signature/relay_server/
lcv1_relay.rs

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