|
| 1 | +//! Marshal fuzz invariants. |
| 2 | +//! |
| 3 | +//! Each function asserts one property of the marshal-under-test against |
| 4 | +//! the driver's shadow state. The orchestrator [`check_all`] runs them |
| 5 | +//! in order; runner.rs only calls `check_all`. |
| 6 | +//! |
| 7 | +//! Conventions match the rest of the consensus fuzz crate: panics on |
| 8 | +//! violation, with a message that includes the relevant shadow state so |
| 9 | +//! libFuzzer's crash log is self-explanatory. |
| 10 | +
|
| 11 | +use commonware_consensus::{marshal::mocks::harness::TestHarness, types::Height}; |
| 12 | +use commonware_cryptography::Digestible; |
| 13 | +use std::collections::{BTreeMap, HashSet}; |
| 14 | + |
| 15 | +/// Run every marshal invariant. Called from the driver at end of run. |
| 16 | +pub fn check_all<H: TestHarness>( |
| 17 | + ready_prefix: u64, |
| 18 | + delivery_log: &[Height], |
| 19 | + segment_bounds: &[usize], |
| 20 | + segment_starts: &[u64], |
| 21 | + expected_redeliveries: &[Vec<Height>], |
| 22 | + application_blocks: &BTreeMap<Height, H::ApplicationBlock>, |
| 23 | + canonical: &[H::TestBlock], |
| 24 | +) { |
| 25 | + check_ready_prefix_delivered(ready_prefix, delivery_log); |
| 26 | + check_segment_ordering(segment_bounds, segment_starts, delivery_log); |
| 27 | + check_redelivery_after_restart(expected_redeliveries, segment_bounds, delivery_log); |
| 28 | + check_digest_fidelity::<H>(application_blocks, canonical); |
| 29 | +} |
| 30 | + |
| 31 | +/// Invariant: ready-prefix delivery. |
| 32 | +/// |
| 33 | +/// Every height in `1..=ready_prefix` must appear at least once in |
| 34 | +/// `delivery_log`. The driver advances `ready_prefix` only when an |
| 35 | +/// above-floor `ReportFinalization` (or restart-triggered repair) |
| 36 | +/// observes a complete chain back to height 1, which is precisely when |
| 37 | +/// marshal is obliged to deliver the prefix. |
| 38 | +pub fn check_ready_prefix_delivered(ready_prefix: u64, delivery_log: &[Height]) { |
| 39 | + let delivered_set: HashSet<u64> = delivery_log.iter().map(|h| h.get()).collect(); |
| 40 | + for h in 1..=ready_prefix { |
| 41 | + assert!( |
| 42 | + delivered_set.contains(&h), |
| 43 | + "marshal violated at-least-once delivery: ready height {h} never reached \ |
| 44 | + the application (ready_prefix={ready_prefix}, delivered={delivered_set:?})", |
| 45 | + ); |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +/// Invariant: per-segment in-order delivery. |
| 50 | +/// |
| 51 | +/// Within each actor instance (segment between restarts) marshal must |
| 52 | +/// deliver heights starting at `restored processed_height + 1` and |
| 53 | +/// advance strictly by one. The driver pre-populates `segment_starts` |
| 54 | +/// from each `setup.height` and `segment_bounds` from the delivery_log |
| 55 | +/// positions at restart boundaries. |
| 56 | +pub fn check_segment_ordering( |
| 57 | + segment_bounds: &[usize], |
| 58 | + segment_starts: &[u64], |
| 59 | + delivery_log: &[Height], |
| 60 | +) { |
| 61 | + assert_eq!( |
| 62 | + segment_bounds.len(), |
| 63 | + segment_starts.len() + 1, |
| 64 | + "segment bookkeeping inconsistency", |
| 65 | + ); |
| 66 | + for (segment_idx, window) in segment_bounds.windows(2).enumerate() { |
| 67 | + let (start_idx, end_idx) = (window[0], window[1]); |
| 68 | + if start_idx == end_idx { |
| 69 | + continue; |
| 70 | + } |
| 71 | + let segment = &delivery_log[start_idx..end_idx]; |
| 72 | + let expected_start = segment_starts[segment_idx]; |
| 73 | + assert_eq!( |
| 74 | + segment[0].get(), |
| 75 | + expected_start, |
| 76 | + "segment #{segment_idx} must start at restored processed height + 1 \ |
| 77 | + ({expected_start}), got {} (segment={:?})", |
| 78 | + segment[0].get(), |
| 79 | + segment, |
| 80 | + ); |
| 81 | + for (offset, h) in segment.iter().enumerate() { |
| 82 | + let expected = expected_start + offset as u64; |
| 83 | + assert_eq!( |
| 84 | + h.get(), |
| 85 | + expected, |
| 86 | + "marshal violated in-order delivery within segment #{segment_idx}: \ |
| 87 | + expected height {expected}, observed {} (segment={:?})", |
| 88 | + h.get(), |
| 89 | + segment, |
| 90 | + ); |
| 91 | + } |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +/// Invariant: at-least-once across restart. |
| 96 | +/// |
| 97 | +/// Each height that was pending ack at the moment of restart `i` must |
| 98 | +/// reappear somewhere in `delivery_log[segment_bounds[i+1]..]`. Their |
| 99 | +/// ack handles were never signaled, so marshal's persistent state |
| 100 | +/// retains them as un-processed and the new instance is obliged to |
| 101 | +/// redeliver. |
| 102 | +pub fn check_redelivery_after_restart( |
| 103 | + expected_redeliveries: &[Vec<Height>], |
| 104 | + segment_bounds: &[usize], |
| 105 | + delivery_log: &[Height], |
| 106 | +) { |
| 107 | + for (restart_idx, expected) in expected_redeliveries.iter().enumerate() { |
| 108 | + if expected.is_empty() { |
| 109 | + continue; |
| 110 | + } |
| 111 | + let post_restart_start = segment_bounds[restart_idx + 1]; |
| 112 | + let post_restart: HashSet<u64> = delivery_log[post_restart_start..] |
| 113 | + .iter() |
| 114 | + .map(|h| h.get()) |
| 115 | + .collect(); |
| 116 | + for h in expected { |
| 117 | + assert!( |
| 118 | + post_restart.contains(&h.get()), |
| 119 | + "marshal violated at-least-once across restart: height {} was \ |
| 120 | + pending at restart #{} but was never redelivered \ |
| 121 | + (post-restart deliveries={post_restart:?})", |
| 122 | + h.get(), |
| 123 | + restart_idx + 1, |
| 124 | + ); |
| 125 | + } |
| 126 | + } |
| 127 | +} |
| 128 | + |
| 129 | +/// Invariant: digest fidelity. |
| 130 | +/// |
| 131 | +/// Every block surfaced in `application.blocks()` must match the |
| 132 | +/// canonical chain digest at its height. Re-emits after restart |
| 133 | +/// overwrite the prior `BTreeMap` entry, so the latest delivery at each |
| 134 | +/// height is the one we compare against canonical. |
| 135 | +pub fn check_digest_fidelity<H: TestHarness>( |
| 136 | + application_blocks: &BTreeMap<Height, H::ApplicationBlock>, |
| 137 | + canonical: &[H::TestBlock], |
| 138 | +) { |
| 139 | + for (height, block) in application_blocks.iter() { |
| 140 | + let canonical_block = &canonical[(height.get() - 1) as usize]; |
| 141 | + assert_eq!( |
| 142 | + block.digest(), |
| 143 | + H::digest(canonical_block), |
| 144 | + "marshal delivered a block whose digest does not match the canonical \ |
| 145 | + chain at height {}", |
| 146 | + height.get(), |
| 147 | + ); |
| 148 | + } |
| 149 | +} |
0 commit comments