1use 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
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_state: StakeTableState,
53
54 voting_stake_table_epoch: Option<EpochNumber>,
56
57 stake_table_capacity: usize,
59
60 should_vote: bool,
62
63 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 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 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 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 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 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#[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}