espresso_node/state_signature/relay_server/
lcv3_relay.rs1use std::{
2 collections::{BTreeSet, HashMap, hash_map::Entry},
3 sync::Arc,
4};
5
6use alloy::primitives::U256;
7use axum::http::StatusCode;
8use hotshot_contract_adapter::light_client::derive_signed_state_digest;
9use hotshot_types::{
10 light_client::{
11 LCV3StateSignatureRequestBody, LCV3StateSignaturesBundle, LightClientState, StateVerKey,
12 },
13 traits::signature_key::LCV3StateSignatureKey,
14};
15
16use super::{RelayError, stake_table_tracker::StakeTableTracker};
17
18#[async_trait::async_trait]
19pub trait LCV3StateRelayServerDataSource {
20 fn get_latest_signature_bundle(&self) -> Result<LCV3StateSignaturesBundle, RelayError>;
24
25 async fn post_signature(
29 &mut self,
30 req: LCV3StateSignatureRequestBody,
31 ) -> Result<(), RelayError>;
32}
33
34pub struct LCV3StateRelayServerState {
36 bundles: HashMap<u64, HashMap<LightClientState, LCV3StateSignaturesBundle>>,
38
39 latest_available_bundle: Option<LCV3StateSignaturesBundle>,
41 latest_block_height: Option<u64>,
43
44 gc_queue: BTreeSet<u64>,
46
47 stake_table_tracker: Arc<StakeTableTracker>,
49}
50
51#[async_trait::async_trait]
52impl LCV3StateRelayServerDataSource for LCV3StateRelayServerState {
53 fn get_latest_signature_bundle(&self) -> Result<LCV3StateSignaturesBundle, RelayError> {
54 self.latest_available_bundle
55 .clone()
56 .ok_or(RelayError::catch_all(
57 StatusCode::NOT_FOUND,
58 "The light client V3 state signatures are not ready.".to_owned(),
59 ))
60 }
61
62 async fn post_signature(
63 &mut self,
64 req: LCV3StateSignatureRequestBody,
65 ) -> Result<(), RelayError> {
66 let block_height = req.state.block_height;
67 if block_height <= self.latest_block_height.unwrap_or(0) {
68 return Ok(());
70 }
71 let stake_table = self
72 .stake_table_tracker
73 .stake_table_info_for_block(block_height)
74 .await
75 .map_err(|e| RelayError::catch_all(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
76 let Some(weight) = stake_table.known_nodes.get(&req.key) else {
77 tracing::warn!("Received LCV3 signature from unknown node: {req}");
78 return Err(RelayError::catch_all(
79 StatusCode::UNAUTHORIZED,
80 "LCV3 signature posted by nodes not on the stake table".to_owned(),
81 ));
82 };
83
84 let signed_state_digest =
86 derive_signed_state_digest(&req.state, &req.next_stake, &req.auth_root);
87 if !<StateVerKey as LCV3StateSignatureKey>::verify_state_sig(
88 &req.key,
89 &req.signature,
90 signed_state_digest,
91 ) {
92 tracing::warn!("Couldn't verify the received LCV3 signature: {req}");
93 return Err(RelayError::catch_all(
94 StatusCode::BAD_REQUEST,
95 "The posted LCV3 signature is not valid.".to_owned(),
96 ));
97 }
98
99 let bundles_at_height = self.bundles.entry(block_height).or_default();
100 self.gc_queue.insert(block_height);
101
102 let bundle = bundles_at_height
103 .entry(req.state)
104 .or_insert(LCV3StateSignaturesBundle {
105 state: req.state,
106 next_stake: req.next_stake,
107 auth_root: req.auth_root,
108 signatures: Default::default(),
109 accumulated_weight: U256::from(0),
110 });
111 tracing::debug!(
112 "Accepting new LCV3 signature for block height {} from {}.",
113 block_height,
114 req.key
115 );
116 match bundle.signatures.entry(req.key) {
117 Entry::Occupied(_) => {
118 return Err(RelayError::catch_all(
120 StatusCode::BAD_REQUEST,
121 "A LCV3 signature of this light client state is already posted at this block \
122 height for this key."
123 .to_owned(),
124 ));
125 },
126 Entry::Vacant(entry) => {
127 entry.insert(req.signature);
128 bundle.accumulated_weight += *weight;
129 },
130 }
131
132 if bundle.accumulated_weight >= stake_table.threshold {
133 tracing::info!(
134 "Light client V3 state signature bundle at block height {} is ready to serve.",
135 block_height
136 );
137 self.latest_block_height = Some(block_height);
138 self.latest_available_bundle = Some(bundle.clone());
139
140 self.prune(block_height);
142 }
143
144 Ok(())
145 }
146}
147
148impl LCV3StateRelayServerState {
149 pub fn prune(&mut self, until_height: u64) {
152 while let Some(&height) = self.gc_queue.first() {
153 if height > until_height {
154 return;
155 }
156 self.bundles.remove(&height);
157 self.gc_queue.pop_first();
158 tracing::debug!(%height, "garbage collected for ");
159 }
160 }
161
162 pub fn new(stake_table_tracker: Arc<StakeTableTracker>) -> Self {
163 Self {
164 bundles: HashMap::new(),
165 latest_available_bundle: None,
166 latest_block_height: None,
167 gc_queue: BTreeSet::new(),
168 stake_table_tracker,
169 }
170 }
171}