1use 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
36pub mod relay_server;
38
39const SIGNATURE_STORAGE_CAPACITY: usize = 100;
41
42#[derive(Debug)]
43pub struct StateSigner<ApiVer: StaticVersionType> {
44 sign_key: StateSignKey,
46
47 ver_key: StateVerKey,
49
50 pub(crate) signatures: Arc<RwLock<StateSignatureMemStorage>>,
53
54 voting_stake_table_state: StakeTableState,
56
57 voting_stake_table_epoch: Option<EpochNumber>,
59
60 stake_table_capacity: usize,
62
63 should_vote: bool,
65
66 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 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 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 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 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#[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#[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}