sequencer/
state_signature.rs

1//! Utilities for generating and storing the most recent light client state signatures.
2
3use std::{
4    collections::{HashMap, VecDeque},
5    sync::Arc,
6};
7
8use alloy::primitives::FixedBytes;
9use async_lock::RwLock;
10use espresso_types::{traits::SequencerPersistence, PubKey};
11use hotshot::types::{Event, EventType, SchnorrPubKey};
12use hotshot_task_impls::helpers::derive_signed_state_digest;
13use hotshot_types::{
14    event::LeafInfo,
15    light_client::{
16        LCV2StateSignatureRequestBody, LCV3StateSignatureRequestBody, LightClientState,
17        StakeTableState, StateSignKey, StateSignature, StateVerKey,
18    },
19    traits::{
20        block_contents::BlockHeader,
21        network::ConnectedNetwork,
22        node_implementation::{NodeType, Versions},
23        signature_key::{LCV1StateSignatureKey, LCV2StateSignatureKey, LCV3StateSignatureKey},
24    },
25    utils::{is_ge_epoch_root, option_epoch_from_block_number},
26};
27use jf_signature::SignatureError;
28use surf_disco::{Client, Url};
29use tide_disco::error::ServerError;
30use vbs::version::StaticVersionType;
31
32use crate::{context::Consensus, SeqTypes};
33
34/// A relay server that's collecting and serving the light client state signatures
35pub mod relay_server;
36
37/// Capacity for the in memory signature storage.
38const SIGNATURE_STORAGE_CAPACITY: usize = 100;
39
40#[derive(Debug)]
41pub struct StateSigner<ApiVer: StaticVersionType> {
42    /// Key for signing a new light client state
43    sign_key: StateSignKey,
44
45    /// Key for verifying a light client state
46    ver_key: StateVerKey,
47
48    /// The most recent light client state signatures
49    signatures: RwLock<StateSignatureMemStorage>,
50
51    /// Commitment for current fixed stake table
52    voting_stake_table: StakeTableState,
53
54    /// epoch for the current stake table state
55    voting_stake_table_epoch: Option<<SeqTypes as NodeType>::Epoch>,
56
57    /// Capacity of the stake table
58    stake_table_capacity: usize,
59
60    /// The state relay server url
61    relay_server_client: Option<Client<ServerError, ApiVer>>,
62}
63
64impl<ApiVer: StaticVersionType> StateSigner<ApiVer> {
65    pub fn new(
66        sign_key: StateSignKey,
67        ver_key: StateVerKey,
68        voting_stake_table: StakeTableState,
69        voting_stake_table_epoch: Option<<SeqTypes as NodeType>::Epoch>,
70        stake_table_capacity: usize,
71    ) -> Self {
72        Self {
73            sign_key,
74            ver_key,
75            voting_stake_table,
76            voting_stake_table_epoch,
77            stake_table_capacity,
78            signatures: Default::default(),
79            relay_server_client: Default::default(),
80        }
81    }
82
83    /// Connect to the given state relay server to send signed HotShot states to.
84    pub fn with_relay_server(mut self, url: Url) -> Self {
85        self.relay_server_client = Some(Client::new(url));
86        self
87    }
88
89    pub(super) async fn handle_event<N, P, V>(
90        &mut self,
91        event: &Event<SeqTypes>,
92        consensus_state: Arc<RwLock<Consensus<N, P, V>>>,
93    ) where
94        N: ConnectedNetwork<PubKey>,
95        P: SequencerPersistence,
96        V: Versions,
97    {
98        let EventType::Decide { leaf_chain, .. } = &event.event else {
99            return;
100        };
101        let Some(LeafInfo { leaf, .. }) = leaf_chain.first() else {
102            return;
103        };
104        match leaf
105            .block_header()
106            .get_light_client_state(leaf.view_number())
107        {
108            Ok(state) => {
109                tracing::debug!("New leaves decided. Latest block height: {}", leaf.height(),);
110
111                let consensus = consensus_state.read().await;
112                let cur_block_height = state.block_height;
113                let blocks_per_epoch = consensus.epoch_height;
114
115                // The last few state updates are handled in the consensus, we do not sign them.
116                if leaf.with_epoch & is_ge_epoch_root(cur_block_height, blocks_per_epoch) {
117                    tracing::debug!("Skipping epoch transition block {cur_block_height}");
118                    return;
119                }
120
121                let Ok(auth_root) = leaf.block_header().auth_root() else {
122                    tracing::error!("Failed to get auth root for light client state");
123                    return;
124                };
125
126                let option_state_epoch = option_epoch_from_block_number::<SeqTypes>(
127                    leaf.with_epoch,
128                    cur_block_height,
129                    blocks_per_epoch,
130                );
131
132                if self.voting_stake_table_epoch != option_state_epoch {
133                    let Ok(membership) = consensus
134                        .membership_coordinator
135                        .stake_table_for_epoch(option_state_epoch)
136                        .await
137                    else {
138                        tracing::error!(
139                            "Failed to get membership for epoch: {:?}",
140                            option_state_epoch
141                        );
142                        return;
143                    };
144                    match membership
145                        .stake_table()
146                        .await
147                        .commitment(self.stake_table_capacity)
148                    {
149                        Ok(stake_table_state) => {
150                            self.voting_stake_table_epoch = option_state_epoch;
151                            self.voting_stake_table = stake_table_state;
152                        },
153                        Err(err) => {
154                            tracing::error!("Failed to compute stake table commitment: {:?}", err);
155                            return;
156                        },
157                    }
158                }
159
160                let Ok(request_body) = self
161                    .get_request_body(&state, &self.voting_stake_table, auth_root)
162                    .await
163                else {
164                    tracing::error!("Failed to sign new state");
165                    return;
166                };
167
168                if let Some(client) = &self.relay_server_client {
169                    if let Err(error) = client
170                        .post::<()>("api/state")
171                        .body_binary(&request_body)
172                        .unwrap()
173                        .send()
174                        .await
175                    {
176                        tracing::error!("Error posting signature to the relay server: {:?}", error);
177                    }
178
179                    if !leaf.with_epoch {
180                        // Before epoch upgrade, we need to sign the state for the legacy light client
181                        let Ok(legacy_signature) = self.legacy_sign_new_state(&state).await else {
182                            tracing::error!("Failed to sign new state for legacy light client");
183                            return;
184                        };
185                        let legacy_request_body = LCV2StateSignatureRequestBody {
186                            key: self.ver_key.clone(),
187                            state,
188                            next_stake: StakeTableState::default(),
189                            signature: legacy_signature,
190                        };
191                        if let Err(error) = client
192                            .post::<()>("api/legacy-state")
193                            .body_binary(&legacy_request_body)
194                            .unwrap()
195                            .send()
196                            .await
197                        {
198                            tracing::error!(
199                                "Error posting signature for legacy light client to the relay \
200                                 server: {:?}",
201                                error
202                            );
203                        }
204                    }
205                }
206            },
207            Err(err) => {
208                tracing::error!("Error generating light client state: {:?}", err)
209            },
210        }
211    }
212
213    /// Return a signature of a light client state at given height.
214    pub async fn get_state_signature(&self, height: u64) -> Option<LCV3StateSignatureRequestBody> {
215        let pool_guard = self.signatures.read().await;
216        pool_guard.get_signature(height)
217    }
218
219    /// Sign the light client state at given height and store it.
220    async fn get_request_body(
221        &self,
222        state: &LightClientState,
223        next_stake_table: &StakeTableState,
224        auth_root: FixedBytes<32>,
225    ) -> Result<LCV3StateSignatureRequestBody, SignatureError> {
226        let signed_state_digest = derive_signed_state_digest(state, next_stake_table, &auth_root);
227        let signature = <SchnorrPubKey as LCV3StateSignatureKey>::sign_state(
228            &self.sign_key,
229            signed_state_digest,
230        )?;
231        let v2signature = <SchnorrPubKey as LCV2StateSignatureKey>::sign_state(
232            &self.sign_key,
233            state,
234            next_stake_table,
235        )?;
236        let request_body = LCV3StateSignatureRequestBody {
237            key: self.ver_key.clone(),
238            state: *state,
239            next_stake: *next_stake_table,
240            signature,
241            v2_signature: v2signature.clone(),
242            auth_root,
243        };
244        let mut pool_guard = self.signatures.write().await;
245        pool_guard.push(state.block_height, request_body.clone());
246        tracing::debug!(
247            "New signature added for block height {}",
248            state.block_height
249        );
250        Ok(request_body)
251    }
252
253    async fn legacy_sign_new_state(
254        &self,
255        state: &LightClientState,
256    ) -> Result<StateSignature, SignatureError> {
257        <SchnorrPubKey as LCV1StateSignatureKey>::sign_state(&self.sign_key, state)
258    }
259}
260
261/// A rolling in-memory storage for the most recent light client state signatures.
262#[derive(Debug, Default)]
263pub struct StateSignatureMemStorage {
264    pool: HashMap<u64, LCV3StateSignatureRequestBody>,
265    deque: VecDeque<u64>,
266}
267
268impl StateSignatureMemStorage {
269    pub fn push(&mut self, height: u64, signature: LCV3StateSignatureRequestBody) {
270        self.pool.insert(height, signature);
271        self.deque.push_back(height);
272        if self.pool.len() > SIGNATURE_STORAGE_CAPACITY {
273            self.pool.remove(&self.deque.pop_front().unwrap());
274        }
275    }
276
277    pub fn get_signature(&self, height: u64) -> Option<LCV3StateSignatureRequestBody> {
278        self.pool.get(&height).cloned()
279    }
280}