Skip to main content

hotshot_new_protocol/
serve.rs

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
21/// The block this node hands to a peer whose payload fetch names it.
22///
23/// The bytes are encoded once, when the block is retained, so answering a
24/// request costs no more than a copy into the response.
25struct RetainedBlock {
26    payload_commitment: VidCommitment2,
27    payload: Arc<[u8]>,
28}
29
30/// The serving half of payload recovery.
31///
32/// What a stalled peer asks for is the payload of the view the quorum is
33/// locked at, so that is what this keeps, and `locked` names it. Payloads
34/// arrive before the certificate that moves the lock onto them, and in a
35/// stall later views keep reconstructing while every lock stays put, so a few
36/// of the most recent are held as candidates until one of them is locked.
37pub struct Server<T: NodeType> {
38    public_key: T::SignatureKey,
39    /// The blocks we can serve, newest last.
40    blocks: BTreeMap<ViewNumber, RetainedBlock>,
41    /// The view we are locked at, whose block is never evicted.
42    locked: Option<ViewNumber>,
43    /// The last view the network timed out on, which is what makes us willing
44    /// to serve at all.
45    timed_out: Option<ViewNumber>,
46}
47
48/// How long after a timeout this node keeps answering requests.
49const SERVE_AFTER_TIMEOUT: u64 = 3;
50
51/// How many blocks to keep.
52///
53/// One is the payload of the view we are locked at, which is what peers ask
54/// for. The rest are candidates waiting for a certificate to move the lock
55/// onto them, so this bounds how late a certificate may be — in payloads
56/// obtained meanwhile, which is roughly one per view — before the block it
57/// certifies has been evicted and the node can no longer serve it.
58///
59/// A count is the wrong shape if blocks ever approach the message size limit;
60/// it would then want to be a byte budget that always keeps the locked block
61/// and the newest.
62const 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    /// Hold on to a freshly obtained block, evicting the earliest one we no
75    /// longer need.
76    ///
77    /// Which of the retained blocks the lock will land on is not known yet:
78    /// payloads arrive before the certificates that move the lock onto them,
79    /// and decoding several views can finish out of order. Keeping the last
80    /// few means the one the lock reaches is still here when it does.
81    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    /// Follow our lock to `view`, so that view's block is the one kept.
103    ///
104    /// A lock only ever moves to a view we reconstructed, so we normally hold
105    /// its block. We do not when the certificate arrived so late that the
106    /// block was evicted, and then this node serves nothing for its lock until
107    /// the lock moves again.
108    pub fn lock_moved(&mut self, view: ViewNumber) {
109        if self.blocks.contains_key(&view) {
110            self.locked = Some(view);
111        }
112    }
113
114    /// Note that the network timed out on `view`.
115    pub fn view_timed_out(&mut self, view: ViewNumber) {
116        self.timed_out = self.timed_out.max(Some(view));
117    }
118
119    /// Unicast the retained block to `sender` if that is what `request` asks for.
120    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        // Silence rather than a refusal: a refusal sends the requester to
130        // another peer at once, which is the last thing to do when the reason
131        // we will not answer has nothing to do with which peer was asked.
132        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    /// Whether a payload fetch is plausible right now.
202    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    /// A freshly obtained block is servable at once, but the lock has not
237    /// reached it: a peer asking is asking for the view we are locked at.
238    #[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    /// Locking a view we hold the block for makes it the one we keep.
248    #[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    /// Every retained block answers for its own view, whether or not the lock
259    /// has reached it.
260    #[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    /// A certificate arriving after later views were reconstructed still
273    /// finds its block, which one slot could not survive.
274    #[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    /// Blocks obtained out of order each take their own place.
287    #[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    /// Eviction takes the earliest block, and never the locked one.
298    #[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}