Skip to main content

commonware_consensus/marshal/core/
floor.rs

1use crate::{
2    marshal::resolver::handler::{Annotation, Key, Request},
3    simplex::types::Finalization,
4    types::{Height, Round},
5};
6use commonware_cryptography::{Digest, certificate::Scheme};
7use commonware_resolver::{Resolver, TargetedResolver};
8use commonware_utils::vec::NonEmptyVec;
9
10/// Durable height and round bounds restored when marshal initializes.
11///
12/// The components are independent retention bounds and need not identify the
13/// same finalization.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub struct Floor {
16    height: Option<Height>,
17    round: Round,
18}
19
20impl Floor {
21    /// Returns the latest durably processed height, if any.
22    pub const fn height(&self) -> Option<Height> {
23        self.height
24    }
25
26    /// Returns the latest durable finalization round floor.
27    pub const fn round(&self) -> Round {
28        self.round
29    }
30}
31
32/// Durable floor state plus any update awaiting its anchor block.
33pub(super) struct State<S: Scheme, C: Digest> {
34    height: Option<Height>,
35    round: Round,
36    pending: Option<Finalization<S, C>>,
37}
38
39impl<S: Scheme, C: Digest> State<S, C> {
40    pub(super) const fn resolved(height: Option<Height>, round: Round) -> Self {
41        Self {
42            height,
43            round,
44            pending: None,
45        }
46    }
47
48    pub(super) const fn awaiting_anchor(
49        height: Option<Height>,
50        round: Round,
51        finalization: Finalization<S, C>,
52    ) -> Self {
53        Self {
54            height,
55            round,
56            pending: Some(finalization),
57        }
58    }
59
60    pub(super) const fn snapshot(&self) -> Floor {
61        Floor {
62            height: self.height,
63            round: self.round,
64        }
65    }
66
67    /// Returns the inclusive height floor. Finalized data at or below it is
68    /// neither stored nor repaired.
69    ///
70    /// Nothing processed maps to zero because height zero is genesis, which is
71    /// anchored at startup or sits below a floor anchor and never carries a
72    /// finalization. Delivery uses the stream cursor, which keeps that state
73    /// distinct.
74    pub(super) const fn processed_height(&self) -> Height {
75        match self.height {
76            Some(height) => height,
77            None => Height::zero(),
78        }
79    }
80
81    pub(super) const fn round(&self) -> Round {
82        self.round
83    }
84
85    pub(super) const fn set_processed_height(&mut self, height: Height) {
86        self.height = Some(height);
87    }
88
89    pub(super) const fn set_processed_round(&mut self, round: Round) {
90        self.round = round;
91    }
92
93    /// Returns true while repair and application dispatch must wait for the floor anchor.
94    pub(super) const fn blocks_progress(&self) -> bool {
95        self.pending.is_some()
96    }
97
98    /// Returns true if a pending floor already supersedes the candidate floor round.
99    pub(super) fn has_pending_anchor_at_or_after(&self, round: Round) -> bool {
100        matches!(&self.pending, Some(pending) if pending.round() >= round)
101    }
102
103    /// Returns true when `commitment` is the awaited anchor.
104    pub(super) fn matches_pending_anchor(&self, commitment: C) -> bool {
105        matches!(&self.pending, Some(pending) if pending.proposal.payload == commitment)
106    }
107
108    /// Records a verified floor finalization whose block anchor still needs to arrive.
109    pub(super) fn await_anchor(&mut self, finalization: Finalization<S, C>) {
110        self.pending = Some(finalization);
111    }
112
113    /// Takes the pending anchor finalization, if any.
114    #[must_use]
115    pub(super) const fn take_pending_anchor(&mut self) -> Option<Finalization<S, C>> {
116        self.pending.take()
117    }
118
119    /// Takes the pending anchor if the processed round floor now covers its round.
120    ///
121    /// Finalized rounds and heights increase together along the finalized chain, so
122    /// an anchor at or below the round floor sits at or below the processed height.
123    #[must_use]
124    pub(super) fn take_superseded_anchor(&mut self, round: Round) -> Option<Finalization<S, C>> {
125        self.pending.take_if(|pending| pending.round() <= round)
126    }
127
128    /// Returns true when the resolver request is above all processed floors.
129    fn permits(&self, fetch: &Request<C>) -> bool {
130        if let Some(height) = self.height
131            && !fetch.above_height_floor(height)
132        {
133            return false;
134        }
135
136        fetch.above_round_floor(self.round)
137    }
138
139    pub(super) fn fetch_if_permitted<R>(
140        &self,
141        resolver: &mut R,
142        fetch: Request<C>,
143    ) -> FetchAdmission
144    where
145        R: Resolver<Key = Key<C>, Subscriber = Annotation>,
146    {
147        if !self.permits(&fetch) {
148            return FetchAdmission::Denied;
149        }
150        resolver.fetch(fetch);
151        FetchAdmission::Issued
152    }
153
154    pub(super) fn fetch_targeted_if_permitted<R>(
155        &self,
156        resolver: &mut R,
157        fetch: Request<C>,
158        targets: NonEmptyVec<R::PublicKey>,
159    ) -> FetchAdmission
160    where
161        R: TargetedResolver<Key = Key<C>, Subscriber = Annotation>,
162    {
163        if !self.permits(&fetch) {
164            return FetchAdmission::Denied;
165        }
166        resolver.fetch_targeted(fetch, targets);
167        FetchAdmission::Issued
168    }
169
170    pub(super) fn fetch_all_if_permitted<R>(
171        &self,
172        resolver: &mut R,
173        fetches: Vec<Request<C>>,
174    ) -> FetchAdmission
175    where
176        R: Resolver<Key = Key<C>, Subscriber = Annotation>,
177    {
178        let fetches = fetches
179            .into_iter()
180            .filter(|fetch| self.permits(fetch))
181            .collect::<Vec<_>>();
182        if fetches.is_empty() {
183            return FetchAdmission::Denied;
184        }
185        resolver.fetch_all(fetches);
186        FetchAdmission::Issued
187    }
188}
189
190/// Whether floor admission issued at least one resolver fetch.
191#[must_use = "fetch admission must be handled explicitly"]
192pub(super) enum FetchAdmission {
193    Issued,
194    Denied,
195}
196
197impl FetchAdmission {
198    pub(super) const fn ignore(self) {}
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::{
205        marshal::resolver::handler::Finalized,
206        simplex::scheme::ed25519 as simplex_ed25519,
207        types::{Epoch, View},
208    };
209    use commonware_actor::Feedback;
210    use commonware_cryptography::{Signer as _, ed25519 as crypto_ed25519, sha256::Sha256};
211    use commonware_math::algebra::Random as _;
212    use commonware_resolver::Fetch;
213    use commonware_utils::sync::Mutex;
214    use std::sync::Arc;
215
216    type TestDigest = <Sha256 as commonware_cryptography::Hasher>::Digest;
217    type TestScheme = simplex_ed25519::Scheme;
218    type FetchRecord = Fetch<Key<TestDigest>, Annotation>;
219    type RecordedFetches = Arc<Mutex<Vec<FetchRecord>>>;
220    type RecordedTargets = Arc<Mutex<Vec<Key<TestDigest>>>>;
221
222    #[derive(Clone, Default)]
223    struct TestResolver {
224        fetches: RecordedFetches,
225        targeted: RecordedTargets,
226    }
227
228    impl TestResolver {
229        fn fetches(&self) -> Vec<FetchRecord> {
230            self.fetches.lock().clone()
231        }
232
233        fn targeted(&self) -> Vec<Key<TestDigest>> {
234            self.targeted.lock().clone()
235        }
236    }
237
238    impl Resolver for TestResolver {
239        type Key = Key<TestDigest>;
240        type Subscriber = Annotation;
241
242        fn fetch<F>(&mut self, fetch: F) -> Feedback
243        where
244            F: Into<Fetch<Self::Key, Self::Subscriber>> + Send,
245        {
246            self.fetches.lock().push(fetch.into());
247            Feedback::Ok
248        }
249
250        fn fetch_all<F>(&mut self, fetches: Vec<F>) -> Feedback
251        where
252            F: Into<Fetch<Self::Key, Self::Subscriber>> + Send,
253        {
254            self.fetches
255                .lock()
256                .extend(fetches.into_iter().map(Into::into));
257            Feedback::Ok
258        }
259
260        fn retain(
261            &mut self,
262            _predicate: impl Fn(&Self::Key, &Self::Subscriber) -> bool + Send + 'static,
263        ) -> Feedback {
264            Feedback::Ok
265        }
266    }
267
268    impl TargetedResolver for TestResolver {
269        type PublicKey = crypto_ed25519::PublicKey;
270
271        fn fetch_targeted(
272            &mut self,
273            fetch: impl Into<Fetch<Self::Key, Self::Subscriber>> + Send,
274            _targets: NonEmptyVec<Self::PublicKey>,
275        ) -> Feedback {
276            self.targeted.lock().push(fetch.into().key);
277            Feedback::Ok
278        }
279
280        fn fetch_all_targeted<F>(
281            &mut self,
282            fetches: Vec<(F, NonEmptyVec<Self::PublicKey>)>,
283        ) -> Feedback
284        where
285            F: Into<Fetch<Self::Key, Self::Subscriber>> + Send,
286        {
287            self.targeted
288                .lock()
289                .extend(fetches.into_iter().map(|(fetch, _)| fetch.into().key));
290            Feedback::Ok
291        }
292    }
293
294    fn round(view: u64) -> Round {
295        Round::new(Epoch::zero(), View::new(view))
296    }
297
298    fn digest(byte: u8) -> TestDigest {
299        Sha256::fill(byte)
300    }
301
302    fn floor() -> State<TestScheme, TestDigest> {
303        State::resolved(Some(Height::new(5)), round(5))
304    }
305
306    #[test]
307    fn fetch_if_permitted_applies_height_and_round_floors() {
308        let floor = floor();
309        let mut resolver = TestResolver::default();
310
311        assert!(matches!(
312            floor.fetch_if_permitted(&mut resolver, Request::finalized(Height::new(5))),
313            FetchAdmission::Denied
314        ));
315        assert!(matches!(
316            floor.fetch_if_permitted(
317                &mut resolver,
318                Request::finalized_block_by_height(digest(1), Height::new(4)),
319            ),
320            FetchAdmission::Denied
321        ));
322        assert!(matches!(
323            floor.fetch_if_permitted(&mut resolver, Request::notarized(round(5))),
324            FetchAdmission::Denied
325        ));
326        assert!(resolver.fetches().is_empty());
327
328        assert!(matches!(
329            floor.fetch_if_permitted(&mut resolver, Request::finalized(Height::new(6))),
330            FetchAdmission::Issued
331        ));
332        assert!(matches!(
333            floor.fetch_if_permitted(&mut resolver, Request::notarized(round(6))),
334            FetchAdmission::Issued
335        ));
336
337        let fetches = resolver.fetches();
338        assert_eq!(fetches.len(), 2);
339        assert!(matches!(
340            fetches[0],
341            Fetch {
342                key: Key::Finalized {
343                    height
344                },
345                subscriber: Annotation::Finalized(Finalized::ByHeight {
346                    height: subscriber_height
347                }),
348                ..
349            } if height == Height::new(6) && subscriber_height == Height::new(6)
350        ));
351        assert!(matches!(
352            fetches[1],
353            Fetch {
354                key: Key::Notarized {
355                    round: request_round
356                },
357                subscriber: Annotation::Notarization {
358                    round: subscriber_round
359                },
360                ..
361            } if request_round == round(6) && subscriber_round == round(6)
362        ));
363    }
364
365    #[test]
366    fn fetch_targeted_if_permitted_returns_denied_without_fetching() {
367        let floor = floor();
368        let mut resolver = TestResolver::default();
369        let mut rng = commonware_utils::test_rng();
370        let target = crypto_ed25519::PrivateKey::random(&mut rng).public_key();
371
372        assert!(matches!(
373            floor.fetch_targeted_if_permitted(
374                &mut resolver,
375                Request::finalized(Height::new(5)),
376                NonEmptyVec::new(target.clone()),
377            ),
378            FetchAdmission::Denied
379        ));
380        assert!(resolver.targeted().is_empty());
381
382        assert!(matches!(
383            floor.fetch_targeted_if_permitted(
384                &mut resolver,
385                Request::finalized(Height::new(6)),
386                NonEmptyVec::new(target),
387            ),
388            FetchAdmission::Issued
389        ));
390        assert_eq!(
391            resolver.targeted(),
392            vec![Key::Finalized {
393                height: Height::new(6)
394            }]
395        );
396    }
397
398    #[test]
399    fn fetch_all_if_permitted_filters_denied_requests() {
400        let floor = floor();
401        let mut resolver = TestResolver::default();
402
403        assert!(matches!(
404            floor.fetch_all_if_permitted(
405                &mut resolver,
406                vec![
407                    Request::finalized(Height::new(5)),
408                    Request::finalized(Height::new(6)),
409                    Request::notarized(round(5)),
410                    Request::notarized(round(6)),
411                ],
412            ),
413            FetchAdmission::Issued
414        ));
415
416        let fetches = resolver.fetches();
417        assert_eq!(fetches.len(), 2);
418        assert!(matches!(fetches[0].key, Key::Finalized { height } if height == Height::new(6)));
419        assert!(
420            matches!(fetches[1].key, Key::Notarized { round: request_round } if request_round == round(6))
421        );
422
423        let mut resolver = TestResolver::default();
424        assert!(matches!(
425            floor.fetch_all_if_permitted(
426                &mut resolver,
427                vec![
428                    Request::finalized(Height::new(5)),
429                    Request::notarized(round(5)),
430                ],
431            ),
432            FetchAdmission::Denied
433        ));
434        assert!(resolver.fetches().is_empty());
435    }
436
437    #[test]
438    fn fetch_if_permitted_without_height_floor_allows_genesis_height() {
439        let floor = State::<TestScheme, TestDigest>::resolved(None, round(5));
440        let mut resolver = TestResolver::default();
441
442        assert!(matches!(
443            floor.fetch_if_permitted(&mut resolver, Request::finalized(Height::zero())),
444            FetchAdmission::Issued
445        ));
446
447        let fetches = resolver.fetches();
448        assert_eq!(fetches.len(), 1);
449        assert!(matches!(
450            fetches[0],
451            Fetch {
452                key: Key::Finalized {
453                    height
454                },
455                subscriber: Annotation::Finalized(Finalized::ByHeight {
456                    height: subscriber_height
457                }),
458                ..
459            } if height == Height::zero() && subscriber_height == Height::zero()
460        ));
461    }
462}