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