linera_core/client/requests_scheduler/
request.rs

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