linera_core/client/requests_scheduler/
request.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt;
5
6use linera_base::{
7    data_types::{Blob, BlobContent, BlockHeight},
8    identifiers::{BlobId, ChainId},
9};
10use linera_chain::types::ConfirmedBlockCertificate;
11
12use crate::{client::requests_scheduler::cache::SubsumingKey, data_types::CompressedHeights};
13
14/// Unique identifier for different types of download requests.
15///
16/// Used for request deduplication to avoid redundant downloads of the same data.
17#[derive(Clone, PartialEq, Eq, Hash)]
18pub enum RequestKey {
19    /// Download certificates by specific heights
20    Certificates {
21        chain_id: ChainId,
22        heights: Vec<BlockHeight>,
23    },
24    /// Download a blob by ID
25    Blob(BlobId),
26    /// Download a pending blob
27    PendingBlob { chain_id: ChainId, blob_id: BlobId },
28    /// Download certificate for a specific blob
29    CertificateForBlob(BlobId),
30}
31
32impl fmt::Debug for RequestKey {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            RequestKey::Certificates { chain_id, heights } => f
36                .debug_struct("Certificates")
37                .field("chain_id", chain_id)
38                .field("heights", &CompressedHeights(heights))
39                .finish(),
40            RequestKey::Blob(blob_id) => f.debug_tuple("Blob").field(blob_id).finish(),
41            RequestKey::PendingBlob { chain_id, blob_id } => f
42                .debug_struct("PendingBlob")
43                .field("chain_id", chain_id)
44                .field("blob_id", blob_id)
45                .finish(),
46            RequestKey::CertificateForBlob(blob_id) => {
47                f.debug_tuple("CertificateForBlob").field(blob_id).finish()
48            }
49        }
50    }
51}
52
53impl RequestKey {
54    /// Returns the chain ID associated with the request, if applicable.
55    pub(super) fn chain_id(&self) -> Option<ChainId> {
56        match self {
57            RequestKey::Certificates { chain_id, .. } => Some(*chain_id),
58            RequestKey::PendingBlob { chain_id, .. } => Some(*chain_id),
59            _ => None,
60        }
61    }
62
63    /// Converts certificate-related requests to a common representation of (chain_id, sorted heights).
64    ///
65    /// This helper method normalizes both `Certificates` and `CertificatesByHeights` variants
66    /// into a uniform format for easier comparison and overlap detection.
67    ///
68    /// # Returns
69    /// - `Some((chain_id, heights))` for certificate requests, where heights are sorted
70    /// - `None` for non-certificate requests (Blob, PendingBlob, CertificateForBlob)
71    fn heights(&self) -> Option<Vec<BlockHeight>> {
72        match self {
73            RequestKey::Certificates { heights, .. } => Some(heights.clone()),
74            _ => None,
75        }
76    }
77}
78
79/// Result types that can be shared across deduplicated requests
80#[derive(Debug, Clone)]
81pub enum RequestResult {
82    Certificates(Vec<ConfirmedBlockCertificate>),
83    Blob(Option<Blob>),
84    BlobContent(BlobContent),
85    Certificate(Box<ConfirmedBlockCertificate>),
86}
87
88/// Marker trait for types that can be converted to/from `RequestResult`
89/// for use in the requests cache.
90pub trait Cacheable: TryFrom<RequestResult> + Into<RequestResult> {}
91impl<T> Cacheable for T where T: TryFrom<RequestResult> + Into<RequestResult> {}
92
93impl From<Option<Blob>> for RequestResult {
94    fn from(blob: Option<Blob>) -> Self {
95        RequestResult::Blob(blob)
96    }
97}
98
99impl From<Vec<ConfirmedBlockCertificate>> for RequestResult {
100    fn from(certs: Vec<ConfirmedBlockCertificate>) -> Self {
101        RequestResult::Certificates(certs)
102    }
103}
104
105impl From<BlobContent> for RequestResult {
106    fn from(content: BlobContent) -> Self {
107        RequestResult::BlobContent(content)
108    }
109}
110
111impl From<ConfirmedBlockCertificate> for RequestResult {
112    fn from(cert: ConfirmedBlockCertificate) -> Self {
113        RequestResult::Certificate(Box::new(cert))
114    }
115}
116
117impl TryFrom<RequestResult> for Option<Blob> {
118    type Error = ();
119
120    fn try_from(result: RequestResult) -> Result<Self, Self::Error> {
121        match result {
122            RequestResult::Blob(blob) => Ok(blob),
123            _ => Err(()),
124        }
125    }
126}
127
128impl TryFrom<RequestResult> for Vec<ConfirmedBlockCertificate> {
129    type Error = ();
130
131    fn try_from(result: RequestResult) -> Result<Self, Self::Error> {
132        match result {
133            RequestResult::Certificates(certs) => Ok(certs),
134            _ => Err(()),
135        }
136    }
137}
138
139impl TryFrom<RequestResult> for BlobContent {
140    type Error = ();
141
142    fn try_from(result: RequestResult) -> Result<Self, Self::Error> {
143        match result {
144            RequestResult::BlobContent(content) => Ok(content),
145            _ => Err(()),
146        }
147    }
148}
149
150impl TryFrom<RequestResult> for ConfirmedBlockCertificate {
151    type Error = ();
152
153    fn try_from(result: RequestResult) -> Result<Self, Self::Error> {
154        match result {
155            RequestResult::Certificate(cert) => Ok(*cert),
156            _ => Err(()),
157        }
158    }
159}
160
161impl SubsumingKey<RequestResult> for super::request::RequestKey {
162    fn subsumes(&self, other: &Self) -> bool {
163        // Different chains can't subsume each other
164        if self.chain_id() != other.chain_id() {
165            return false;
166        }
167
168        let (in_flight_req_heights, new_req_heights) = match (self.heights(), other.heights()) {
169            (Some(range1), Some(range2)) => (range1, range2),
170            _ => return false, // We subsume only certificate requests
171        };
172
173        let mut in_flight_req_heights_iter = in_flight_req_heights.into_iter();
174
175        for new_height in new_req_heights {
176            if !in_flight_req_heights_iter.any(|h| h == new_height) {
177                return false; // Found a height not covered by in-flight request
178            }
179        }
180        true
181    }
182
183    fn try_extract_result(
184        &self,
185        in_flight_request: &RequestKey,
186        result: &RequestResult,
187    ) -> Option<RequestResult> {
188        // Only certificate results can be extracted
189        let certificates = match result {
190            RequestResult::Certificates(certs) => certs,
191            _ => return None,
192        };
193
194        if !in_flight_request.subsumes(self) {
195            return None; // Can't extract if not subsumed
196        }
197
198        let mut requested_heights = self.heights()?;
199        if requested_heights.is_empty() {
200            return Some(RequestResult::Certificates(vec![])); // Nothing requested
201        }
202        let mut certificates_iter = certificates.iter();
203        let mut collected = vec![];
204        while let Some(height) = requested_heights.first() {
205            // Remove certs below the requested height.
206            if let Some(cert) = certificates_iter.find(|cert| &cert.value().height() == height) {
207                collected.push(cert.clone());
208                requested_heights.remove(0);
209            } else {
210                return None; // Missing a requested height
211            }
212        }
213
214        Some(RequestResult::Certificates(collected))
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use linera_base::{crypto::CryptoHash, data_types::BlockHeight, identifiers::ChainId};
221
222    use super::{RequestKey, SubsumingKey};
223
224    #[test]
225    fn test_subsumes_complete_containment() {
226        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
227        let large = RequestKey::Certificates {
228            chain_id,
229            heights: vec![BlockHeight(11), BlockHeight(12), BlockHeight(13)],
230        };
231        let small = RequestKey::Certificates {
232            chain_id,
233            heights: vec![BlockHeight(12)],
234        };
235        assert!(large.subsumes(&small));
236        assert!(!small.subsumes(&large));
237    }
238
239    #[test]
240    fn test_subsumes_partial_containment() {
241        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
242        let req1 = RequestKey::Certificates {
243            chain_id,
244            heights: vec![BlockHeight(12), BlockHeight(13)],
245        };
246        let req2 = RequestKey::Certificates {
247            chain_id,
248            heights: vec![BlockHeight(12), BlockHeight(14)],
249        };
250        assert!(!req1.subsumes(&req2));
251        assert!(!req2.subsumes(&req1));
252    }
253
254    #[test]
255    fn test_subsumes_different_chains() {
256        let chain1 = ChainId(CryptoHash::test_hash("chain1"));
257        let chain2 = ChainId(CryptoHash::test_hash("chain2"));
258        let req1 = RequestKey::Certificates {
259            chain_id: chain1,
260            heights: vec![BlockHeight(12)],
261        };
262        let req2 = RequestKey::Certificates {
263            chain_id: chain2,
264            heights: vec![BlockHeight(12)],
265        };
266        assert!(!req1.subsumes(&req2));
267    }
268
269    // Helper function to create a test certificate at a specific height
270    fn make_test_cert(
271        height: u64,
272        chain_id: ChainId,
273    ) -> linera_chain::types::ConfirmedBlockCertificate {
274        use linera_base::{
275            crypto::ValidatorKeypair,
276            data_types::{Round, Timestamp},
277        };
278        use linera_chain::{
279            block::ConfirmedBlock,
280            data_types::{BlockExecutionOutcome, LiteValue, LiteVote},
281            test::{make_first_block, BlockTestExt, VoteTestExt},
282        };
283
284        let keypair = ValidatorKeypair::generate();
285        let mut proposed_block = make_first_block(chain_id).with_timestamp(Timestamp::from(height));
286
287        // Set the correct height
288        proposed_block.height = BlockHeight(height);
289
290        // Create a Block from the proposed block with default execution outcome
291        let block = BlockExecutionOutcome::default().with(proposed_block);
292
293        // Create a ConfirmedBlock
294        let confirmed_block = ConfirmedBlock::new(block);
295
296        // Create a LiteVote and convert to Vote
297        let lite_vote = LiteVote::new(
298            LiteValue::new(&confirmed_block),
299            Round::MultiLeader(0),
300            &keypair.secret_key,
301        );
302
303        // Convert to full vote
304        let vote = lite_vote.with_value(confirmed_block).unwrap();
305
306        // Convert vote to certificate
307        vote.into_certificate(keypair.secret_key.public())
308    }
309
310    #[test]
311    fn test_try_extract_result_non_certificate_result() {
312        use super::RequestResult;
313
314        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
315        let req1 = RequestKey::Certificates {
316            chain_id,
317            heights: vec![BlockHeight(12)],
318        };
319        let req2 = RequestKey::Certificates {
320            chain_id,
321            heights: vec![BlockHeight(12)],
322        };
323
324        // Non-certificate result should return None
325        let blob_result = RequestResult::Blob(None);
326        assert!(req1.try_extract_result(&req2, &blob_result).is_none());
327    }
328
329    #[test]
330    fn test_try_extract_result_empty_request_range() {
331        use super::RequestResult;
332
333        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
334        let req1 = RequestKey::Certificates {
335            chain_id,
336            heights: vec![],
337        };
338        let req2 = RequestKey::Certificates {
339            chain_id,
340            heights: vec![BlockHeight(10)],
341        };
342
343        let certs = vec![make_test_cert(10, chain_id)];
344        let result = RequestResult::Certificates(certs);
345
346        // Empty request is always extractable, should return empty result
347        match req1.try_extract_result(&req2, &result) {
348            Some(RequestResult::Certificates(extracted_certs)) => {
349                assert!(extracted_certs.is_empty());
350            }
351            _ => panic!("Expected Some empty Certificates result"),
352        }
353    }
354
355    #[test]
356    fn test_try_extract_result_empty_result_range() {
357        use super::RequestResult;
358
359        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
360        let req1 = RequestKey::Certificates {
361            chain_id,
362            heights: vec![BlockHeight(12)],
363        };
364        let req2 = RequestKey::Certificates {
365            chain_id,
366            heights: vec![BlockHeight(12)],
367        };
368
369        let result = RequestResult::Certificates(vec![]); // Empty result
370
371        // Empty result should return None
372        assert!(req1.try_extract_result(&req2, &result).is_none());
373    }
374
375    #[test]
376    fn test_try_extract_result_non_overlapping_ranges() {
377        use super::RequestResult;
378
379        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
380        let new_req = RequestKey::Certificates {
381            chain_id,
382            heights: vec![BlockHeight(10)],
383        };
384        let in_flight_req = RequestKey::Certificates {
385            chain_id,
386            heights: vec![BlockHeight(11)],
387        };
388
389        // Result does not contain all requested heights
390        let certs = vec![make_test_cert(11, chain_id)];
391        let result = RequestResult::Certificates(certs);
392
393        // No overlap, should return None
394        assert!(new_req
395            .try_extract_result(&in_flight_req, &result)
396            .is_none());
397    }
398
399    #[test]
400    fn test_try_extract_result_partial_overlap_missing_start() {
401        use super::RequestResult;
402
403        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
404        let req1 = RequestKey::Certificates {
405            chain_id,
406            heights: vec![BlockHeight(10), BlockHeight(11), BlockHeight(12)],
407        };
408        let req2 = RequestKey::Certificates {
409            chain_id,
410            heights: vec![BlockHeight(11), BlockHeight(12)],
411        };
412
413        // Result missing the first height (10)
414        let certs = vec![make_test_cert(11, chain_id), make_test_cert(12, chain_id)];
415        let result = RequestResult::Certificates(certs);
416
417        // Missing start height, should return None
418        assert!(req1.try_extract_result(&req2, &result).is_none());
419    }
420
421    #[test]
422    fn test_try_extract_result_partial_overlap_missing_end() {
423        use super::RequestResult;
424
425        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
426        let req1 = RequestKey::Certificates {
427            chain_id,
428            heights: vec![BlockHeight(10), BlockHeight(11), BlockHeight(12)],
429        };
430        let req2 = RequestKey::Certificates {
431            chain_id,
432            heights: vec![BlockHeight(10), BlockHeight(11)],
433        };
434
435        // Result missing the last height (14)
436        let certs = vec![make_test_cert(10, chain_id), make_test_cert(11, chain_id)];
437        let result = RequestResult::Certificates(certs);
438
439        // Missing end height, should return None
440        assert!(req1.try_extract_result(&req2, &result).is_none());
441    }
442
443    #[test]
444    fn test_try_extract_result_partial_overlap_missing_middle() {
445        use super::RequestResult;
446
447        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
448        let new_req = RequestKey::Certificates {
449            chain_id,
450            heights: vec![BlockHeight(10), BlockHeight(12), BlockHeight(13)],
451        };
452        let in_flight_req = RequestKey::Certificates {
453            chain_id,
454            heights: vec![
455                BlockHeight(10),
456                BlockHeight(12),
457                BlockHeight(13),
458                BlockHeight(14),
459            ],
460        };
461
462        let certs = vec![
463            make_test_cert(10, chain_id),
464            make_test_cert(13, chain_id),
465            make_test_cert(14, chain_id),
466        ];
467        let result = RequestResult::Certificates(certs);
468
469        assert!(new_req
470            .try_extract_result(&in_flight_req, &result)
471            .is_none());
472        assert!(in_flight_req
473            .try_extract_result(&new_req, &result)
474            .is_none());
475    }
476
477    #[test]
478    fn test_try_extract_result_exact_match() {
479        use super::RequestResult;
480
481        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
482        let req1 = RequestKey::Certificates {
483            chain_id,
484            heights: vec![BlockHeight(10), BlockHeight(11), BlockHeight(12)],
485        }; // [10, 11, 12]
486        let req2 = RequestKey::Certificates {
487            chain_id,
488            heights: vec![BlockHeight(10), BlockHeight(11), BlockHeight(12)],
489        };
490
491        let certs = vec![
492            make_test_cert(10, chain_id),
493            make_test_cert(11, chain_id),
494            make_test_cert(12, chain_id),
495        ];
496        let result = RequestResult::Certificates(certs.clone());
497
498        // Exact match should return all certificates
499        let extracted = req1.try_extract_result(&req2, &result);
500        assert!(extracted.is_some());
501        match extracted.unwrap() {
502            RequestResult::Certificates(extracted_certs) => {
503                assert_eq!(extracted_certs, certs);
504            }
505            _ => panic!("Expected Certificates result"),
506        }
507    }
508
509    #[test]
510    fn test_try_extract_result_superset_extraction() {
511        use super::RequestResult;
512
513        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
514        let req1 = RequestKey::Certificates {
515            chain_id,
516            heights: vec![BlockHeight(12), BlockHeight(13)],
517        };
518        let req2 = RequestKey::Certificates {
519            chain_id,
520            heights: vec![BlockHeight(12), BlockHeight(13)],
521        };
522
523        // Result has more certificates than requested
524        let certs = vec![
525            make_test_cert(10, chain_id),
526            make_test_cert(11, chain_id),
527            make_test_cert(12, chain_id),
528            make_test_cert(13, chain_id),
529            make_test_cert(14, chain_id),
530        ];
531        let result = RequestResult::Certificates(certs);
532
533        // Should extract only the requested range [12, 13]
534        let extracted = req1.try_extract_result(&req2, &result);
535        assert!(extracted.is_some());
536        match extracted.unwrap() {
537            RequestResult::Certificates(extracted_certs) => {
538                assert_eq!(extracted_certs.len(), 2);
539                assert_eq!(extracted_certs[0].value().height(), BlockHeight(12));
540                assert_eq!(extracted_certs[1].value().height(), BlockHeight(13));
541            }
542            _ => panic!("Expected Certificates result"),
543        }
544    }
545
546    #[test]
547    fn test_try_extract_result_single_height() {
548        use super::RequestResult;
549
550        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
551        let req1 = RequestKey::Certificates {
552            chain_id,
553            heights: vec![BlockHeight(15)],
554        }; // [15]
555        let req2 = RequestKey::Certificates {
556            chain_id,
557            heights: vec![BlockHeight(10), BlockHeight(15), BlockHeight(20)],
558        };
559
560        let certs = vec![
561            make_test_cert(10, chain_id),
562            make_test_cert(15, chain_id),
563            make_test_cert(20, chain_id),
564        ];
565        let result = RequestResult::Certificates(certs);
566
567        // Should extract only height 15
568        let extracted = req1.try_extract_result(&req2, &result);
569        assert!(extracted.is_some());
570        match extracted.unwrap() {
571            RequestResult::Certificates(extracted_certs) => {
572                assert_eq!(extracted_certs.len(), 1);
573                assert_eq!(extracted_certs[0].value().height(), BlockHeight(15));
574            }
575            _ => panic!("Expected Certificates result"),
576        }
577    }
578
579    #[test]
580    fn test_try_extract_result_different_chains() {
581        use super::RequestResult;
582
583        let chain1 = ChainId(CryptoHash::test_hash("chain1"));
584        let chain2 = ChainId(CryptoHash::test_hash("chain2"));
585        let req1 = RequestKey::Certificates {
586            chain_id: chain1,
587            heights: vec![BlockHeight(12)],
588        };
589        let req2 = RequestKey::Certificates {
590            chain_id: chain2,
591            heights: vec![BlockHeight(12)],
592        };
593
594        let certs = vec![make_test_cert(12, chain1)];
595        let result = RequestResult::Certificates(certs);
596
597        // Different chains should return None
598        assert!(req1.try_extract_result(&req2, &result).is_none());
599    }
600}