1use std::{collections::BTreeMap, sync::Arc};
2
3use hotshot_types::{
4 data::{VidCommitment2, ViewNumber},
5 traits::{block_contents::EncodeBytes, node_implementation::NodeType},
6 vote::HasViewNumber,
7};
8use tracing::{debug, warn};
9
10use crate::{
11 coordinator::error::CoordinatorError,
12 message::{
13 Message, MessageType,
14 payload::{
15 PayloadFetchMessage, PayloadFetchRequest, PayloadFetchResponse, PayloadResponseBody,
16 },
17 },
18 network::{NetworkError, Sender},
19};
20
21struct RetainedBlock {
26 payload_commitment: VidCommitment2,
27 payload: Arc<[u8]>,
28}
29
30pub struct Server<T: NodeType> {
38 public_key: T::SignatureKey,
39 blocks: BTreeMap<ViewNumber, RetainedBlock>,
41 locked: Option<ViewNumber>,
43 timed_out: Option<ViewNumber>,
46}
47
48const SERVE_AFTER_TIMEOUT: u64 = 3;
50
51const RETAINED_BLOCKS: usize = 4;
63
64impl<T: NodeType> Server<T> {
65 pub fn new(public_key: T::SignatureKey) -> Self {
66 Self {
67 public_key,
68 blocks: BTreeMap::new(),
69 locked: None,
70 timed_out: None,
71 }
72 }
73
74 pub fn retain(
82 &mut self,
83 view: ViewNumber,
84 payload_commitment: VidCommitment2,
85 payload: &T::BlockPayload,
86 ) {
87 self.blocks.insert(
88 view,
89 RetainedBlock {
90 payload_commitment,
91 payload: payload.encode(),
92 },
93 );
94
95 while self.blocks.len() > RETAINED_BLOCKS
96 && let Some(&v) = self.blocks.keys().find(|v| Some(**v) != self.locked)
97 {
98 self.blocks.remove(&v);
99 }
100 }
101
102 pub fn lock_moved(&mut self, view: ViewNumber) {
109 if self.blocks.contains_key(&view) {
110 self.locked = Some(view);
111 }
112 }
113
114 pub fn view_timed_out(&mut self, view: ViewNumber) {
116 self.timed_out = self.timed_out.max(Some(view));
117 }
118
119 pub fn handle_request(
121 &self,
122 request: &PayloadFetchRequest,
123 sender: &T::SignatureKey,
124 slot: ViewNumber,
125 network: &Sender<T>,
126 ) -> Result<(), CoordinatorError> {
127 let view = request.view_number();
128
129 if !self.serving(slot) {
133 debug!(%view, %sender, "payload request outside a timeout window");
134 return Ok(());
135 }
136
137 let Some(block) = self.blocks.get(&view) else {
138 debug!(%view, %sender, "payload request for a block we do not retain");
139 return self.respond(
140 view,
141 PayloadResponseBody::NotAvailable,
142 sender,
143 slot,
144 network,
145 );
146 };
147
148 if block.payload.len() >= network.max_message_size().get() {
149 warn!(
150 %view,
151 %sender,
152 bytes = %block.payload.len(),
153 limit = %network.max_message_size(),
154 "retained payload does not fit a message"
155 );
156 return self.respond(view, PayloadResponseBody::TooLarge, sender, slot, network);
157 }
158
159 let message = Message {
160 sender: self.public_key.clone(),
161 message_type: {
162 let body = PayloadResponseBody::Payload {
163 commitment: block.payload_commitment,
164 data: block.payload.to_vec(),
165 };
166 let res = PayloadFetchResponse::new(view, body);
167 MessageType::PayloadFetch(PayloadFetchMessage::Res(res))
168 },
169 };
170
171 match network.unicast(slot, sender, &message) {
172 Ok(()) => Ok(()),
173 Err(NetworkError::Cliquenet(cliquenet::NetworkError::MessageTooLarge)) => {
174 warn!(%view, %sender, "payload response rejected as too large");
175 self.respond(view, PayloadResponseBody::TooLarge, sender, slot, network)
176 },
177 Err(err) => Err(CoordinatorError::from(err).context("payload response")),
178 }
179 }
180
181 fn respond(
182 &self,
183 view: ViewNumber,
184 body: PayloadResponseBody,
185 sender: &T::SignatureKey,
186 slot: ViewNumber,
187 network: &Sender<T>,
188 ) -> Result<(), CoordinatorError> {
189 let message = Message {
190 sender: self.public_key.clone(),
191 message_type: {
192 let res = PayloadFetchResponse::new(view, body);
193 MessageType::PayloadFetch(PayloadFetchMessage::Res(res))
194 },
195 };
196 network
197 .unicast(slot, sender, &message)
198 .map_err(|err| CoordinatorError::from(err).context("payload response"))
199 }
200
201 fn serving(&self, current_view: ViewNumber) -> bool {
203 self.timed_out
204 .is_some_and(|view| current_view <= view + SERVE_AFTER_TIMEOUT)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use hotshot_example_types::{
211 block_types::{TestBlockPayload, TestTransaction},
212 node_types::TestTypes,
213 };
214 use hotshot_types::{
215 data::{VidCommitment2, ViewNumber},
216 signature_key::BLSPubKey,
217 traits::signature_key::SignatureKey,
218 };
219
220 use super::{RETAINED_BLOCKS, Server};
221
222 fn server() -> Server<TestTypes> {
223 Server::new(BLSPubKey::generated_from_seed_indexed([0; 32], 0).0)
224 }
225
226 fn payload() -> TestBlockPayload {
227 TestBlockPayload {
228 transactions: vec![TestTransaction::new(vec![1, 2, 3])],
229 }
230 }
231
232 fn view(n: u64) -> ViewNumber {
233 ViewNumber::new(n)
234 }
235
236 #[test]
239 fn retaining_a_block_does_not_move_the_lock() {
240 let mut server = server();
241 server.retain(view(1), VidCommitment2::default(), &payload());
242
243 assert!(server.locked.is_none());
244 assert!(server.blocks.contains_key(&view(1)));
245 }
246
247 #[test]
249 fn locking_a_retained_view_keeps_its_block() {
250 let mut server = server();
251 server.retain(view(1), VidCommitment2::default(), &payload());
252 server.lock_moved(view(1));
253
254 assert_eq!(server.locked, Some(view(1)));
255 assert!(server.blocks.contains_key(&view(1)));
256 }
257
258 #[test]
261 fn any_retained_block_serves_its_view() {
262 let mut server = server();
263 server.retain(view(1), VidCommitment2::default(), &payload());
264 server.lock_moved(view(1));
265 server.retain(view(2), VidCommitment2::default(), &payload());
266
267 assert!(server.blocks.contains_key(&view(1)));
268 assert!(server.blocks.contains_key(&view(2)));
269 assert!(!server.blocks.contains_key(&view(3)));
270 }
271
272 #[test]
275 fn a_late_certificate_still_finds_its_block() {
276 let mut server = server();
277 server.retain(view(1), VidCommitment2::default(), &payload());
278 server.retain(view(2), VidCommitment2::default(), &payload());
279 server.retain(view(3), VidCommitment2::default(), &payload());
280 server.lock_moved(view(1));
281
282 assert_eq!(server.locked, Some(view(1)));
283 assert!(server.blocks.contains_key(&view(1)));
284 }
285
286 #[test]
288 fn an_earlier_block_does_not_displace_a_later_one() {
289 let mut server = server();
290 server.retain(view(2), VidCommitment2::default(), &payload());
291 server.retain(view(1), VidCommitment2::default(), &payload());
292
293 assert!(server.blocks.contains_key(&view(1)));
294 assert!(server.blocks.contains_key(&view(2)));
295 }
296
297 #[test]
299 fn eviction_spares_the_locked_block() {
300 let mut server = server();
301 server.retain(view(1), VidCommitment2::default(), &payload());
302 server.lock_moved(view(1));
303 for v in 2..=(RETAINED_BLOCKS as u64 + 2) {
304 server.retain(view(v), VidCommitment2::default(), &payload());
305 }
306
307 assert_eq!(server.blocks.len(), RETAINED_BLOCKS);
308 assert!(
309 server.blocks.contains_key(&view(1)),
310 "the locked block is never evicted"
311 );
312 assert!(
313 !server.blocks.contains_key(&view(2)),
314 "the earliest unlocked block goes first"
315 );
316 }
317}