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