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