Skip to main content

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, if present.
206            collected.push(
207                certificates_iter
208                    .find(|cert| &cert.value().height() == height)?
209                    .clone(),
210            );
211            requested_heights.remove(0);
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            justification::JustificationChain,
282            test::{make_first_block, BlockTestExt, VoteTestExt},
283            types::ConfirmedBlockCertificate,
284        };
285
286        let keypair = ValidatorKeypair::generate();
287        let mut proposed_block = make_first_block(chain_id).with_timestamp(Timestamp::from(height));
288
289        // Set the correct height
290        proposed_block.height = BlockHeight(height);
291
292        // Create a Block from the proposed block with default execution outcome
293        let block = BlockExecutionOutcome::default().with(proposed_block);
294
295        // Create a ConfirmedBlock
296        let confirmed_block = ConfirmedBlock::new(block);
297
298        // Create a LiteVote and convert to Vote
299        let lite_vote = LiteVote::new(
300            LiteValue::new(&confirmed_block),
301            Round::MultiLeader(0),
302            &keypair.secret_key,
303        );
304
305        // Convert to full vote
306        let vote = lite_vote.with_value(confirmed_block).unwrap();
307
308        // Convert vote to certificate
309        let quorum = vote.into_certificate(keypair.secret_key.public());
310        ConfirmedBlockCertificate::from_parts(quorum, JustificationChain::default())
311    }
312
313    #[test]
314    fn test_try_extract_result_non_certificate_result() {
315        use super::RequestResult;
316
317        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
318        let req1 = RequestKey::Certificates {
319            chain_id,
320            heights: vec![BlockHeight(12)],
321        };
322        let req2 = RequestKey::Certificates {
323            chain_id,
324            heights: vec![BlockHeight(12)],
325        };
326
327        // Non-certificate result should return None
328        let blob_result = RequestResult::Blob(None);
329        assert!(req1.try_extract_result(&req2, &blob_result).is_none());
330    }
331
332    #[test]
333    fn test_try_extract_result_empty_request_range() {
334        use super::RequestResult;
335
336        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
337        let req1 = RequestKey::Certificates {
338            chain_id,
339            heights: vec![],
340        };
341        let req2 = RequestKey::Certificates {
342            chain_id,
343            heights: vec![BlockHeight(10)],
344        };
345
346        let certs = vec![make_test_cert(10, chain_id)];
347        let result = RequestResult::Certificates(certs);
348
349        // Empty request is always extractable, should return empty result
350        match req1.try_extract_result(&req2, &result) {
351            Some(RequestResult::Certificates(extracted_certs)) => {
352                assert!(extracted_certs.is_empty());
353            }
354            _ => panic!("Expected Some empty Certificates result"),
355        }
356    }
357
358    #[test]
359    fn test_try_extract_result_empty_result_range() {
360        use super::RequestResult;
361
362        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
363        let req1 = RequestKey::Certificates {
364            chain_id,
365            heights: vec![BlockHeight(12)],
366        };
367        let req2 = RequestKey::Certificates {
368            chain_id,
369            heights: vec![BlockHeight(12)],
370        };
371
372        let result = RequestResult::Certificates(vec![]); // Empty result
373
374        // Empty result should return None
375        assert!(req1.try_extract_result(&req2, &result).is_none());
376    }
377
378    #[test]
379    fn test_try_extract_result_non_overlapping_ranges() {
380        use super::RequestResult;
381
382        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
383        let new_req = RequestKey::Certificates {
384            chain_id,
385            heights: vec![BlockHeight(10)],
386        };
387        let in_flight_req = RequestKey::Certificates {
388            chain_id,
389            heights: vec![BlockHeight(11)],
390        };
391
392        // Result does not contain all requested heights
393        let certs = vec![make_test_cert(11, chain_id)];
394        let result = RequestResult::Certificates(certs);
395
396        // No overlap, should return None
397        assert!(new_req
398            .try_extract_result(&in_flight_req, &result)
399            .is_none());
400    }
401
402    #[test]
403    fn test_try_extract_result_partial_overlap_missing_start() {
404        use super::RequestResult;
405
406        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
407        let req1 = RequestKey::Certificates {
408            chain_id,
409            heights: vec![BlockHeight(10), BlockHeight(11), BlockHeight(12)],
410        };
411        let req2 = RequestKey::Certificates {
412            chain_id,
413            heights: vec![BlockHeight(11), BlockHeight(12)],
414        };
415
416        // Result missing the first height (10)
417        let certs = vec![make_test_cert(11, chain_id), make_test_cert(12, chain_id)];
418        let result = RequestResult::Certificates(certs);
419
420        // Missing start height, should return None
421        assert!(req1.try_extract_result(&req2, &result).is_none());
422    }
423
424    #[test]
425    fn test_try_extract_result_partial_overlap_missing_end() {
426        use super::RequestResult;
427
428        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
429        let req1 = RequestKey::Certificates {
430            chain_id,
431            heights: vec![BlockHeight(10), BlockHeight(11), BlockHeight(12)],
432        };
433        let req2 = RequestKey::Certificates {
434            chain_id,
435            heights: vec![BlockHeight(10), BlockHeight(11)],
436        };
437
438        // Result missing the last height (14)
439        let certs = vec![make_test_cert(10, chain_id), make_test_cert(11, chain_id)];
440        let result = RequestResult::Certificates(certs);
441
442        // Missing end height, should return None
443        assert!(req1.try_extract_result(&req2, &result).is_none());
444    }
445
446    #[test]
447    fn test_try_extract_result_partial_overlap_missing_middle() {
448        use super::RequestResult;
449
450        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
451        let new_req = RequestKey::Certificates {
452            chain_id,
453            heights: vec![BlockHeight(10), BlockHeight(12), BlockHeight(13)],
454        };
455        let in_flight_req = RequestKey::Certificates {
456            chain_id,
457            heights: vec![
458                BlockHeight(10),
459                BlockHeight(12),
460                BlockHeight(13),
461                BlockHeight(14),
462            ],
463        };
464
465        let certs = vec![
466            make_test_cert(10, chain_id),
467            make_test_cert(13, chain_id),
468            make_test_cert(14, chain_id),
469        ];
470        let result = RequestResult::Certificates(certs);
471
472        assert!(new_req
473            .try_extract_result(&in_flight_req, &result)
474            .is_none());
475        assert!(in_flight_req
476            .try_extract_result(&new_req, &result)
477            .is_none());
478    }
479
480    #[test]
481    fn test_try_extract_result_exact_match() {
482        use super::RequestResult;
483
484        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
485        let req1 = RequestKey::Certificates {
486            chain_id,
487            heights: vec![BlockHeight(10), BlockHeight(11), BlockHeight(12)],
488        }; // [10, 11, 12]
489        let req2 = RequestKey::Certificates {
490            chain_id,
491            heights: vec![BlockHeight(10), BlockHeight(11), BlockHeight(12)],
492        };
493
494        let certs = vec![
495            make_test_cert(10, chain_id),
496            make_test_cert(11, chain_id),
497            make_test_cert(12, chain_id),
498        ];
499        let result = RequestResult::Certificates(certs.clone());
500
501        // Exact match should return all certificates
502        let extracted = req1.try_extract_result(&req2, &result);
503        assert!(extracted.is_some());
504        match extracted.unwrap() {
505            RequestResult::Certificates(extracted_certs) => {
506                assert_eq!(extracted_certs, certs);
507            }
508            _ => panic!("Expected Certificates result"),
509        }
510    }
511
512    #[test]
513    fn test_try_extract_result_superset_extraction() {
514        use super::RequestResult;
515
516        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
517        let req1 = RequestKey::Certificates {
518            chain_id,
519            heights: vec![BlockHeight(12), BlockHeight(13)],
520        };
521        let req2 = RequestKey::Certificates {
522            chain_id,
523            heights: vec![BlockHeight(12), BlockHeight(13)],
524        };
525
526        // Result has more certificates than requested
527        let certs = vec![
528            make_test_cert(10, chain_id),
529            make_test_cert(11, chain_id),
530            make_test_cert(12, chain_id),
531            make_test_cert(13, chain_id),
532            make_test_cert(14, chain_id),
533        ];
534        let result = RequestResult::Certificates(certs);
535
536        // Should extract only the requested range [12, 13]
537        let extracted = req1.try_extract_result(&req2, &result);
538        assert!(extracted.is_some());
539        match extracted.unwrap() {
540            RequestResult::Certificates(extracted_certs) => {
541                assert_eq!(extracted_certs.len(), 2);
542                assert_eq!(extracted_certs[0].value().height(), BlockHeight(12));
543                assert_eq!(extracted_certs[1].value().height(), BlockHeight(13));
544            }
545            _ => panic!("Expected Certificates result"),
546        }
547    }
548
549    #[test]
550    fn test_try_extract_result_single_height() {
551        use super::RequestResult;
552
553        let chain_id = ChainId(CryptoHash::test_hash("chain1"));
554        let req1 = RequestKey::Certificates {
555            chain_id,
556            heights: vec![BlockHeight(15)],
557        }; // [15]
558        let req2 = RequestKey::Certificates {
559            chain_id,
560            heights: vec![BlockHeight(10), BlockHeight(15), BlockHeight(20)],
561        };
562
563        let certs = vec![
564            make_test_cert(10, chain_id),
565            make_test_cert(15, chain_id),
566            make_test_cert(20, chain_id),
567        ];
568        let result = RequestResult::Certificates(certs);
569
570        // Should extract only height 15
571        let extracted = req1.try_extract_result(&req2, &result);
572        assert!(extracted.is_some());
573        match extracted.unwrap() {
574            RequestResult::Certificates(extracted_certs) => {
575                assert_eq!(extracted_certs.len(), 1);
576                assert_eq!(extracted_certs[0].value().height(), BlockHeight(15));
577            }
578            _ => panic!("Expected Certificates result"),
579        }
580    }
581
582    #[test]
583    fn test_try_extract_result_different_chains() {
584        use super::RequestResult;
585
586        let chain1 = ChainId(CryptoHash::test_hash("chain1"));
587        let chain2 = ChainId(CryptoHash::test_hash("chain2"));
588        let req1 = RequestKey::Certificates {
589            chain_id: chain1,
590            heights: vec![BlockHeight(12)],
591        };
592        let req2 = RequestKey::Certificates {
593            chain_id: chain2,
594            heights: vec![BlockHeight(12)],
595        };
596
597        let certs = vec![make_test_cert(12, chain1)];
598        let result = RequestResult::Certificates(certs);
599
600        // Different chains should return None
601        assert!(req1.try_extract_result(&req2, &result).is_none());
602    }
603}