Skip to main content

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