1use std::collections::{HashMap, HashSet};
2
3use hotshot_types::{
4 data::{VidCommitment2, ViewNumber},
5 epoch_membership::EpochMembershipCoordinator,
6 traits::{
7 BlockPayload,
8 block_contents::{BlockHeader, EncodeBytes},
9 node_implementation::NodeType,
10 signature_key::SignatureKey,
11 },
12 vote::HasViewNumber,
13};
14use tokio::task::JoinSet;
15use tracing::{debug, error, warn};
16
17use crate::{
18 consensus::Consensus,
19 message::{
20 Message, MessageType, Validated,
21 fetch::Request,
22 payload::{
23 PayloadFetchMessage, PayloadFetchResponse, PayloadRequestBody, PayloadResponseBody,
24 },
25 },
26 vid::{ObtainedPayload, expected_vid_param, matches_commitment},
27};
28
29pub struct Fetcher<T: NodeType> {
31 public_key: T::SignatureKey,
32
33 requested: HashMap<(ViewNumber, VidCommitment2), Fetch<T::SignatureKey>>,
35
36 tasks: JoinSet<Option<ObtainedPayload<T>>>,
38}
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum Retry {
42 NewRound,
43 SameRound,
44}
45
46struct Fetch<K> {
51 asked: HashSet<K>,
52 pending: HashSet<K>,
53}
54
55impl<K> Default for Fetch<K> {
56 fn default() -> Self {
57 Self {
58 asked: HashSet::new(),
59 pending: HashSet::new(),
60 }
61 }
62}
63
64impl<T: NodeType> Fetcher<T> {
65 pub fn new(public_key: T::SignatureKey) -> Self {
66 Self {
67 public_key,
68 requested: HashMap::new(),
69 tasks: JoinSet::new(),
70 }
71 }
72
73 pub async fn next(&mut self) -> Option<ObtainedPayload<T>> {
74 loop {
75 match self.tasks.join_next().await? {
76 Ok(Some(out)) => return Some(out),
77 Ok(None) => {},
78 Err(err) => {
79 if err.is_panic() {
80 error!(%err, "fetched payload verification panicked");
81 }
82 },
83 }
84 }
85 }
86
87 pub fn request(
88 &mut self,
89 view: ViewNumber,
90 commitment: VidCommitment2,
91 retry: Retry,
92 consensus: &Consensus<T>,
93 membership: &EpochMembershipCoordinator<T>,
94 ) -> Option<(T::SignatureKey, Message<T, Validated>)> {
95 let Some(target) = self.select_peer(view, commitment, retry, consensus, membership) else {
96 warn!(%view, "no peer to fetch the payload from");
97 return None;
98 };
99
100 let fetch = self.requested.entry((view, commitment)).or_default();
101
102 fetch.asked.insert(target.clone());
103 fetch.pending.insert(target.clone());
104
105 let message = Message {
106 sender: self.public_key.clone(),
107 message_type: {
108 let request = Request::new(view, PayloadRequestBody);
109 MessageType::PayloadFetch(PayloadFetchMessage::Req(request))
110 },
111 };
112
113 Some((target, message))
114 }
115
116 pub fn response(
117 &mut self,
118 response: PayloadFetchResponse,
119 sender: &T::SignatureKey,
120 consensus: &Consensus<T>,
121 membership: &EpochMembershipCoordinator<T>,
122 ) -> Vec<VidCommitment2> {
123 let view = response.view_number();
124 match response.into_body() {
125 PayloadResponseBody::NotAvailable | PayloadResponseBody::TooLarge => {
126 let refused = self.refused(view, sender);
127 if refused.is_empty() {
128 warn!(%view, %sender, "payload refusal from unexpected peer");
129 }
130 refused
131 },
132 PayloadResponseBody::Payload {
133 commitment: payload_commitment,
134 data,
135 } => {
136 if !self
140 .requested
141 .get(&(view, payload_commitment))
142 .is_some_and(|fetch| fetch.pending.contains(sender))
143 {
144 let refused = self.refused(view, sender);
145 if refused.is_empty() {
146 warn!(%view, %sender, "payload response from unexpected peer");
147 } else {
148 debug!(%view, %sender, "payload response for another payload than asked");
149 }
150 return refused;
151 }
152
153 let Some(proposal) = consensus.proposal_with_payload(view, payload_commitment)
154 else {
155 warn!(%view, "payload response does not match a proposal's commitment");
156 return Vec::new();
157 };
158
159 if consensus.is_reconstructed(view, payload_commitment) {
160 debug!(%view, "payload already obtained; dropping the response");
161 if let Some(fetch) = self.requested.get_mut(&(view, payload_commitment)) {
162 fetch.pending.remove(sender);
163 }
164 return Vec::new();
165 }
166
167 if let Some(fetch) = self.requested.get_mut(&(view, payload_commitment)) {
168 fetch.pending.remove(sender);
169 }
170
171 let Some(param) = expected_vid_param(membership, proposal.epoch) else {
172 warn!(%view, "no VID param for fetched payload; dropping");
173 return Vec::new();
174 };
175
176 let epoch = proposal.epoch;
177 let metadata = proposal.block_header.metadata().clone();
178
179 self.tasks.spawn_blocking(move || {
180 if !matches_commitment(
181 view,
182 ¶m,
183 &metadata.encode(),
184 &data,
185 &payload_commitment,
186 ) {
187 return None;
188 }
189 let payload = T::BlockPayload::from_bytes(&data, &metadata);
190 let tx_commitments = payload.transaction_commitments(&metadata);
191 Some(ObtainedPayload {
192 view,
193 epoch,
194 payload_commitment,
195 payload,
196 metadata,
197 tx_commitments,
198 })
199 });
200
201 Vec::new()
202 },
203 }
204 }
205
206 pub fn gc(&mut self, view: ViewNumber) {
207 self.requested.retain(|(v, _), _| *v > view);
208 }
209
210 fn refused(&mut self, view: ViewNumber, sender: &T::SignatureKey) -> Vec<VidCommitment2> {
217 self.requested
218 .iter_mut()
219 .filter(|((asked_view, _), _)| *asked_view == view)
220 .filter_map(|((_, commitment), fetch)| {
221 fetch.pending.remove(sender).then_some(*commitment)
222 })
223 .collect()
224 }
225
226 fn select_peer(
227 &mut self,
228 view: ViewNumber,
229 payload_commitment: VidCommitment2,
230 retry: Retry,
231 consensus: &Consensus<T>,
232 membership: &EpochMembershipCoordinator<T>,
233 ) -> Option<T::SignatureKey> {
234 use rand::seq::IteratorRandom;
235
236 let epoch = consensus
237 .proposal_with_payload(view, payload_commitment)?
238 .epoch;
239 let membership = membership.stake_table_for_epoch(Some(epoch)).ok()?;
240
241 let mut rng = rand::thread_rng();
242 let mut fetch = self.requested.get_mut(&(view, payload_commitment));
243 let me = &self.public_key;
244
245 let draw = |fetch: Option<&Fetch<T::SignatureKey>>, rng: &mut _| {
246 membership
247 .stake_table()
248 .map(|peer| T::SignatureKey::public_key(&peer.stake_table_entry))
249 .filter(|peer| peer != me && fetch.is_none_or(|fetch| !fetch.asked.contains(peer)))
250 .choose(rng)
251 };
252
253 if let Some(peer) = draw(fetch.as_deref(), &mut rng) {
254 return Some(peer);
255 }
256
257 if retry == Retry::SameRound {
258 return None;
259 }
260
261 let fetch = fetch.as_mut()?;
262 fetch.asked.clear();
263
264 draw(None, &mut rng)
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use std::collections::HashSet;
271
272 use hotshot_example_types::{
273 block_types::{TestBlockHeader, TestBlockPayload, TestMetadata, TestTransaction},
274 node_types::{TEST_VERSIONS, TestTypes},
275 };
276 use hotshot_types::{
277 data::{EpochNumber, Leaf2, VidCommitment, VidCommitment2, ViewNumber, ns_table},
278 signature_key::BLSPubKey,
279 traits::{block_contents::EncodeBytes, signature_key::SignatureKey},
280 vid::avidm_gf2::AvidmGf2Scheme,
281 };
282
283 use super::{Fetch, Fetcher, Retry};
284 use crate::{
285 message::{
286 fetch::Response,
287 payload::{PayloadFetchResponse, PayloadResponseBody},
288 },
289 tests::common::utils::{ConsensusHarness, TestData},
290 vid::expected_vid_param,
291 };
292
293 fn key(index: u64) -> BLSPubKey {
294 BLSPubKey::generated_from_seed_indexed([0; 32], index).0
295 }
296
297 fn view() -> ViewNumber {
298 ViewNumber::new(1)
299 }
300
301 async fn payload_and_consensus() -> (Vec<u8>, VidCommitment2, ConsensusHarness) {
307 let mut harness = ConsensusHarness::new(0).await;
308 let test_data = TestData::new(1).await;
309
310 let metadata = TestMetadata {
311 num_transactions: 1,
312 };
313 let payload = TestBlockPayload {
314 transactions: vec![TestTransaction::new(vec![1, 2, 3])],
315 };
316 let bytes = payload.encode().to_vec();
317
318 let param = expected_vid_param(&harness.membership_coordinator, EpochNumber::genesis())
319 .expect("committee resolves");
320 let ns_table = ns_table::parse_ns_table(bytes.len(), &metadata.encode());
321 let (commitment, _) = AvidmGf2Scheme::commit(¶m, &bytes, ns_table).expect("commit");
322
323 let template = &test_data.views[0];
324 let parent_leaf: Leaf2<TestTypes> = template.proposal.data.clone().into();
325 let mut proposal = template.proposal.data.clone();
326 proposal.block_header = TestBlockHeader::new(
327 &parent_leaf,
328 VidCommitment::V2(commitment),
329 template
330 .proposal
331 .data
332 .block_header
333 .builder_commitment
334 .clone(),
335 metadata,
336 TEST_VERSIONS.test.base,
337 );
338
339 harness.force_set_proposal(view(), proposal);
340 (bytes, commitment, harness)
341 }
342
343 fn fetcher_expecting(peer: BLSPubKey, commitment: VidCommitment2) -> Fetcher<TestTypes> {
344 let mut fetcher = Fetcher::new(key(0));
345 fetcher.requested.insert(
346 (view(), commitment),
347 Fetch {
348 asked: HashSet::from([peer]),
349 pending: HashSet::from([peer]),
350 },
351 );
352 fetcher
353 }
354
355 fn payload_response(commitment: VidCommitment2, data: Vec<u8>) -> PayloadFetchResponse {
356 Response::new(view(), PayloadResponseBody::Payload { commitment, data })
357 }
358
359 fn accept(
360 fetcher: &mut Fetcher<TestTypes>,
361 response: PayloadFetchResponse,
362 sender: BLSPubKey,
363 harness: &ConsensusHarness,
364 ) -> bool {
365 !fetcher
366 .response(
367 response,
368 &sender,
369 &harness.consensus,
370 &harness.membership_coordinator,
371 )
372 .is_empty()
373 }
374
375 #[tokio::test]
378 async fn a_payload_that_recommits_is_yielded() {
379 let (bytes, commitment, harness) = payload_and_consensus().await;
380 let peer = key(1);
381 let mut fetcher = fetcher_expecting(peer, commitment);
382
383 assert!(!accept(
384 &mut fetcher,
385 payload_response(commitment, bytes),
386 peer,
387 &harness
388 ));
389
390 let obtained = fetcher.next().await.expect("payload is verified");
391 assert_eq!(obtained.view, view());
392 assert_eq!(obtained.payload_commitment, commitment);
393 }
394
395 #[tokio::test]
398 async fn a_payload_that_does_not_recommit_is_dropped() {
399 let (mut bytes, commitment, harness) = payload_and_consensus().await;
400 bytes.push(0);
401 let peer = key(1);
402 let mut fetcher = fetcher_expecting(peer, commitment);
403
404 accept(
405 &mut fetcher,
406 payload_response(commitment, bytes),
407 peer,
408 &harness,
409 );
410
411 assert!(fetcher.next().await.is_none());
412 }
413
414 #[tokio::test]
417 async fn an_unsolicited_payload_is_not_verified() {
418 let (bytes, commitment, harness) = payload_and_consensus().await;
419 let mut fetcher = fetcher_expecting(key(1), commitment);
420
421 assert!(!accept(
422 &mut fetcher,
423 payload_response(commitment, bytes),
424 key(2),
425 &harness
426 ));
427
428 assert!(fetcher.next().await.is_none());
429 assert!(
430 fetcher
431 .requested
432 .get(&(view(), commitment))
433 .is_some_and(|fetch| fetch.pending.contains(&key(1))),
434 "the peer we asked still owes us an answer"
435 );
436 }
437
438 #[tokio::test]
445 async fn a_response_for_another_commitment_is_a_refusal() {
446 let (bytes, commitment, harness) = payload_and_consensus().await;
447 let peer = key(1);
448 let mut fetcher = fetcher_expecting(peer, commitment);
449
450 assert!(accept(
451 &mut fetcher,
452 payload_response(VidCommitment2::default(), bytes.clone()),
453 peer,
454 &harness
455 ));
456 assert!(
457 !fetcher
458 .requested
459 .get(&(view(), commitment))
460 .is_some_and(|fetch| fetch.pending.contains(&peer)),
461 "the peer no longer owes us an answer"
462 );
463 assert!(!accept(
464 &mut fetcher,
465 payload_response(VidCommitment2::default(), bytes),
466 peer,
467 &harness
468 ));
469
470 assert!(fetcher.next().await.is_none());
471 }
472
473 #[tokio::test]
476 async fn answering_consumes_the_peers_slot() {
477 let (bytes, commitment, harness) = payload_and_consensus().await;
478 let peer = key(1);
479 let mut fetcher = fetcher_expecting(peer, commitment);
480
481 accept(
482 &mut fetcher,
483 payload_response(commitment, bytes.clone()),
484 peer,
485 &harness,
486 );
487 accept(
488 &mut fetcher,
489 payload_response(commitment, bytes),
490 peer,
491 &harness,
492 );
493
494 assert!(fetcher.next().await.is_some());
495 assert!(
496 fetcher.next().await.is_none(),
497 "the second response was not verified"
498 );
499 }
500
501 #[tokio::test]
504 async fn the_draw_excludes_this_node_and_the_peers_already_asked() {
505 let (_, commitment, harness) = payload_and_consensus().await;
506 let mut fetcher = Fetcher::new(key(0));
507 let mut drawn = HashSet::new();
508
509 for _ in 0..9 {
511 let peer = fetcher
512 .select_peer(
513 view(),
514 commitment,
515 Retry::SameRound,
516 &harness.consensus,
517 &harness.membership_coordinator,
518 )
519 .expect("a peer this round has not asked");
520 assert!(peer != key(0), "never ourselves");
521 assert!(drawn.insert(peer), "never the same peer twice");
522 fetcher
523 .requested
524 .entry((view(), commitment))
525 .or_default()
526 .asked
527 .insert(peer);
528 }
529 assert_eq!(drawn.len(), 9);
530 }
531
532 #[tokio::test]
537 async fn only_a_new_round_starts_the_draw_over() {
538 let (_, commitment, harness) = payload_and_consensus().await;
539 let mut fetcher = Fetcher::new(key(0));
540 let everyone: HashSet<BLSPubKey> = (1..10).map(key).collect();
541 fetcher.requested.insert(
542 (view(), commitment),
543 Fetch {
544 asked: everyone.clone(),
545 pending: HashSet::new(),
546 },
547 );
548
549 assert!(
550 fetcher
551 .select_peer(
552 view(),
553 commitment,
554 Retry::SameRound,
555 &harness.consensus,
556 &harness.membership_coordinator,
557 )
558 .is_none(),
559 "the round is exhausted"
560 );
561
562 let peer = fetcher
563 .select_peer(
564 view(),
565 commitment,
566 Retry::NewRound,
567 &harness.consensus,
568 &harness.membership_coordinator,
569 )
570 .expect("a new round draws again");
571 assert!(everyone.contains(&peer));
572 assert!(
573 fetcher.requested[&(view(), commitment)].asked.is_empty(),
574 "the new round forgot who was asked"
575 );
576 }
577
578 #[tokio::test]
582 async fn only_a_peer_we_asked_redraws_and_only_once() {
583 let (_, commitment, harness) = payload_and_consensus().await;
584 let peer = key(1);
585 let mut fetcher = fetcher_expecting(peer, commitment);
586 let refusal = || Response::new(view(), PayloadResponseBody::NotAvailable);
587
588 assert!(!accept(&mut fetcher, refusal(), key(2), &harness));
589 assert!(accept(&mut fetcher, refusal(), peer, &harness));
590 assert!(
591 !accept(&mut fetcher, refusal(), peer, &harness),
592 "the slot was spent by the first refusal"
593 );
594 assert!(
595 fetcher
596 .requested
597 .get(&(view(), commitment))
598 .is_some_and(|fetch| fetch.asked.contains(&peer)),
599 "a spent peer is still not drawn again"
600 );
601 }
602}