1use 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
38pub mod relay_server;
40
41const SIGNATURE_STORAGE_CAPACITY: usize = 100;
43
44#[derive(Debug)]
45pub struct StateSigner<ApiVer: StaticVersionType> {
46 sign_key: StateSignKey,
48
49 ver_key: StateVerKey,
51
52 signatures: RwLock<StateSignatureMemStorage>,
54
55 voting_stake_table_state: StakeTableState,
57
58 voting_stake_table_epoch: Option<EpochNumber>,
60
61 stake_table_capacity: usize,
63
64 should_vote: bool,
66
67 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 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 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 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 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 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#[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}