1use 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
34pub mod relay_server;
36
37const SIGNATURE_STORAGE_CAPACITY: usize = 100;
39
40#[derive(Debug)]
41pub struct StateSigner<ApiVer: StaticVersionType> {
42 sign_key: StateSignKey,
44
45 ver_key: StateVerKey,
47
48 signatures: RwLock<StateSignatureMemStorage>,
50
51 voting_stake_table: StakeTableState,
53
54 voting_stake_table_epoch: Option<<SeqTypes as NodeType>::Epoch>,
56
57 stake_table_capacity: usize,
59
60 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 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 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 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 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 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#[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}