Skip to main content

ic_memory/
physical.rs

1use serde::{Deserialize, Serialize};
2
3const COMMIT_MARKER: u64 = 0x4943_4D45_4D43_4F4D;
4const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
5const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
6
7///
8/// ProtectedGenerationSlot
9///
10/// One physical generation slot that can participate in protected recovery.
11///
12/// This is an advanced low-level API for framework or stable-IO owners. Most
13/// callers should use the ledger commit/recovery flow instead of implementing
14/// physical slot recovery directly.
15pub trait ProtectedGenerationSlot: Eq {
16    /// Generation encoded by this slot.
17    fn generation(&self) -> u64;
18
19    /// Return whether the slot passed its marker/checksum validation.
20    fn validates(&self) -> bool;
21}
22
23///
24/// DualProtectedCommitStore
25///
26/// Physical store with two protected generation slots.
27///
28/// This is an advanced low-level API for framework or stable-IO owners. Normal
29/// allocation flows recover and commit ledgers through the higher-level ledger
30/// commit APIs.
31pub trait DualProtectedCommitStore {
32    /// Protected slot record type.
33    type Slot: ProtectedGenerationSlot;
34
35    /// Borrow the first physical slot.
36    fn slot0(&self) -> Option<&Self::Slot>;
37
38    /// Borrow the second physical slot.
39    fn slot1(&self) -> Option<&Self::Slot>;
40
41    /// Return true when no commit slot has ever been written.
42    fn is_uninitialized(&self) -> bool {
43        self.slot0().is_none() && self.slot1().is_none()
44    }
45
46    /// Return the highest-generation valid physical slot.
47    fn authoritative_slot(&self) -> Result<AuthoritativeSlot<'_, Self::Slot>, CommitRecoveryError> {
48        select_authoritative_slot(self.slot0(), self.slot1())
49    }
50
51    /// Return the slot that should receive the next staged generation write.
52    ///
53    /// The result is derived from validated recovery state. It does not trust a
54    /// separate current-pointer/header field.
55    fn inactive_slot_index(&self) -> CommitSlotIndex {
56        match self.authoritative_slot() {
57            Ok(authoritative) => authoritative.index.opposite(),
58            Err(_) => CommitSlotIndex::Slot0,
59        }
60    }
61}
62
63///
64/// CommitSlotIndex
65///
66/// Physical dual-slot index selected by protected recovery.
67#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
68pub enum CommitSlotIndex {
69    /// First physical commit slot.
70    Slot0,
71    /// Second physical commit slot.
72    Slot1,
73}
74
75impl CommitSlotIndex {
76    /// Return the opposite physical slot.
77    #[must_use]
78    pub const fn opposite(self) -> Self {
79        match self {
80            Self::Slot0 => Self::Slot1,
81            Self::Slot1 => Self::Slot0,
82        }
83    }
84}
85
86///
87/// AuthoritativeSlot
88///
89/// Highest-generation valid slot selected by protected recovery.
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91pub struct AuthoritativeSlot<'slot, T> {
92    /// Physical slot index.
93    pub index: CommitSlotIndex,
94    /// Valid committed generation in that slot.
95    pub record: &'slot T,
96}
97
98/// Select the highest-generation valid physical slot.
99///
100/// This is an advanced recovery helper for framework or stable-IO owners. It
101/// only selects among supplied protected slots; it does not decode or validate
102/// the allocation ledger payload.
103pub fn select_authoritative_slot<'slot, T: ProtectedGenerationSlot>(
104    slot0: Option<&'slot T>,
105    slot1: Option<&'slot T>,
106) -> Result<AuthoritativeSlot<'slot, T>, CommitRecoveryError> {
107    let slot0 = slot0
108        .filter(|slot| slot.validates())
109        .map(|record| AuthoritativeSlot {
110            index: CommitSlotIndex::Slot0,
111            record,
112        });
113    let slot1 = slot1
114        .filter(|slot| slot.validates())
115        .map(|record| AuthoritativeSlot {
116            index: CommitSlotIndex::Slot1,
117            record,
118        });
119
120    match (slot0, slot1) {
121        (Some(left), Some(right))
122            if left.record.generation() == right.record.generation()
123                && left.record != right.record =>
124        {
125            Err(CommitRecoveryError::AmbiguousGeneration {
126                generation: left.record.generation(),
127            })
128        }
129        (Some(left), Some(right)) if right.record.generation() > left.record.generation() => {
130            Ok(right)
131        }
132        (Some(left), Some(_) | None) => Ok(left),
133        (None, Some(right)) => Ok(right),
134        (None, None) => Err(CommitRecoveryError::NoValidGeneration),
135    }
136}
137
138///
139/// CommittedGenerationBytes
140///
141/// Physically committed ledger generation payload protected by a checksum.
142///
143/// This is an advanced low-level DTO for framework or stable-IO owners. Its
144/// recovered bytes are untrusted until marker/checksum validation and ledger
145/// decoding/integrity validation have both succeeded.
146#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
147#[serde(deny_unknown_fields)]
148pub struct CommittedGenerationBytes {
149    /// Generation number represented by this payload.
150    pub(crate) generation: u64,
151    /// Physical commit marker. Readers reject records with an invalid marker.
152    pub(crate) commit_marker: u64,
153    /// Checksum over the generation, marker, and payload bytes.
154    pub(crate) checksum: u64,
155    /// Encoded ledger generation payload.
156    pub(crate) payload: Vec<u8>,
157}
158
159impl CommittedGenerationBytes {
160    /// Build a committed generation record.
161    #[must_use]
162    pub fn new(generation: u64, payload: Vec<u8>) -> Self {
163        let mut record = Self {
164            generation,
165            commit_marker: COMMIT_MARKER,
166            checksum: 0,
167            payload,
168        };
169        record.checksum = generation_checksum(&record);
170        record
171    }
172
173    /// Return the generation number represented by this payload.
174    #[must_use]
175    pub const fn generation(&self) -> u64 {
176        self.generation
177    }
178
179    /// Return the physical commit marker.
180    ///
181    /// This is diagnostic data from a recovered record. Callers should use
182    /// [`CommittedGenerationBytes::validates`] before treating the record as
183    /// authoritative.
184    #[must_use]
185    pub const fn commit_marker(&self) -> u64 {
186        self.commit_marker
187    }
188
189    /// Return the checksum over the generation, marker, and payload bytes.
190    ///
191    /// The checksum is non-cryptographic and detects torn writes or accidental
192    /// corruption only.
193    #[must_use]
194    pub const fn checksum(&self) -> u64 {
195        self.checksum
196    }
197
198    /// Borrow the encoded ledger generation payload.
199    #[must_use]
200    pub fn payload(&self) -> &[u8] {
201        &self.payload
202    }
203
204    /// Return whether the marker and checksum validate.
205    #[must_use]
206    pub fn validates(&self) -> bool {
207        self.commit_marker == COMMIT_MARKER && self.checksum == generation_checksum(self)
208    }
209}
210
211impl ProtectedGenerationSlot for CommittedGenerationBytes {
212    fn generation(&self) -> u64 {
213        self.generation
214    }
215
216    fn validates(&self) -> bool {
217        self.validates()
218    }
219}
220
221///
222/// DualCommitStore
223///
224/// Dual-slot protected commit protocol for encoded ledger generations.
225///
226/// This is an advanced low-level API for framework or stable-IO owners. Most
227/// applications should recover, validate, and commit through the allocation
228/// ledger flow rather than manipulating encoded physical commit slots directly.
229///
230/// Writers stage a complete generation record into the inactive slot. Readers
231/// recover by selecting the highest-generation valid slot. A torn or partial
232/// write cannot become authoritative unless its marker and checksum validate.
233///
234/// The checksum is for torn-write and accidental-corruption detection only. It
235/// is not a cryptographic hash and does not provide adversarial tamper
236/// resistance.
237#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
238#[serde(deny_unknown_fields)]
239pub struct DualCommitStore {
240    /// First physical commit slot.
241    pub(crate) slot0: Option<CommittedGenerationBytes>,
242    /// Second physical commit slot.
243    pub(crate) slot1: Option<CommittedGenerationBytes>,
244}
245
246impl DualCommitStore {
247    /// Return true when no commit slot has ever been written.
248    #[must_use]
249    pub const fn is_uninitialized(&self) -> bool {
250        self.slot0.is_none() && self.slot1.is_none()
251    }
252
253    /// Borrow the first physical commit slot.
254    ///
255    /// Slot records are untrusted recovered state until recovery selects an
256    /// authoritative generation.
257    #[must_use]
258    pub const fn slot0(&self) -> Option<&CommittedGenerationBytes> {
259        self.slot0.as_ref()
260    }
261
262    /// Borrow the second physical commit slot.
263    ///
264    /// Slot records are untrusted recovered state until recovery selects an
265    /// authoritative generation.
266    #[must_use]
267    pub const fn slot1(&self) -> Option<&CommittedGenerationBytes> {
268        self.slot1.as_ref()
269    }
270
271    /// Return the highest-generation valid committed record.
272    pub fn authoritative(&self) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
273        self.authoritative_slot()
274            .map(|authoritative| authoritative.record)
275    }
276
277    /// Build a read-only recovery diagnostic for the protected commit slots.
278    #[must_use]
279    pub fn diagnostic(&self) -> CommitStoreDiagnostic {
280        CommitStoreDiagnostic::from_store(self)
281    }
282
283    /// Commit a new payload to the inactive slot.
284    ///
285    /// The returned store models the post-write physical state. If a real
286    /// substrate traps before the inactive slot is fully written, the prior
287    /// valid slot remains authoritative under `authoritative`.
288    pub fn commit_payload(
289        &mut self,
290        payload: Vec<u8>,
291    ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
292        let next_generation =
293            match self.authoritative() {
294                Ok(record) => record.generation.checked_add(1).ok_or(
295                    CommitRecoveryError::GenerationOverflow {
296                        generation: record.generation,
297                    },
298                )?,
299                Err(CommitRecoveryError::NoValidGeneration) if self.is_uninitialized() => 0,
300                Err(err) => return Err(err),
301            };
302
303        self.commit_payload_at_generation(next_generation, payload)
304    }
305
306    /// Commit `payload` as an explicitly numbered physical generation.
307    ///
308    /// This is the low-level physical-slot primitive used by
309    /// [`crate::LedgerCommitStore`]. Normal ledger commits should use
310    /// [`crate::LedgerCommitStore::commit`] or [`crate::AllocationBootstrap`] so
311    /// payloads are decoded, current-format checked, and integrity-validated
312    /// before they can become authoritative.
313    ///
314    /// The physical slot generation is checked against the recovered physical
315    /// predecessor. This method does not inspect `payload`.
316    pub fn commit_payload_at_generation(
317        &mut self,
318        generation: u64,
319        payload: Vec<u8>,
320    ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
321        match self.authoritative() {
322            Ok(record) => {
323                let expected = record.generation.checked_add(1).ok_or(
324                    CommitRecoveryError::GenerationOverflow {
325                        generation: record.generation,
326                    },
327                )?;
328                if generation != expected {
329                    return Err(CommitRecoveryError::UnexpectedGeneration {
330                        expected,
331                        actual: generation,
332                    });
333                }
334            }
335            Err(CommitRecoveryError::NoValidGeneration) if self.is_uninitialized() => {}
336            Err(err) => return Err(err),
337        }
338
339        let next = CommittedGenerationBytes::new(generation, payload);
340
341        if self.inactive_slot_index() == CommitSlotIndex::Slot0 {
342            self.slot0 = Some(next);
343        } else {
344            self.slot1 = Some(next);
345        }
346
347        self.authoritative()
348    }
349
350    /// Simulate a torn write into the inactive slot.
351    ///
352    /// This helper is intentionally part of the model because recovery behavior
353    /// is an ABI requirement, not an implementation detail.
354    #[cfg(test)]
355    pub fn write_corrupt_inactive_slot(&mut self, generation: u64, payload: Vec<u8>) {
356        let mut corrupt = CommittedGenerationBytes::new(generation, payload);
357        corrupt.checksum = corrupt.checksum.wrapping_add(1);
358
359        if self.inactive_slot_index() == CommitSlotIndex::Slot0 {
360            self.slot0 = Some(corrupt);
361        } else {
362            self.slot1 = Some(corrupt);
363        }
364    }
365}
366
367impl DualProtectedCommitStore for DualCommitStore {
368    type Slot = CommittedGenerationBytes;
369
370    fn slot0(&self) -> Option<&Self::Slot> {
371        self.slot0.as_ref()
372    }
373
374    fn slot1(&self) -> Option<&Self::Slot> {
375        self.slot1.as_ref()
376    }
377}
378
379///
380/// CommitStoreDiagnostic
381///
382/// Read-only diagnostic summary of protected commit recovery state.
383#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
384#[serde(deny_unknown_fields)]
385pub struct CommitStoreDiagnostic {
386    /// First physical commit slot diagnostic.
387    pub slot0: CommitSlotDiagnostic,
388    /// Second physical commit slot diagnostic.
389    pub slot1: CommitSlotDiagnostic,
390    /// Highest valid generation selected by recovery.
391    pub authoritative_generation: Option<u64>,
392    /// Recovery error when no authoritative generation can be selected.
393    pub recovery_error: Option<CommitRecoveryError>,
394}
395
396impl CommitStoreDiagnostic {
397    /// Build a read-only recovery diagnostic from a dual protected commit store.
398    #[must_use]
399    pub fn from_store<S: DualProtectedCommitStore>(store: &S) -> Self {
400        let (authoritative_generation, recovery_error) = match store.authoritative_slot() {
401            Ok(slot) => (Some(slot.record.generation()), None),
402            Err(err) => (None, Some(err)),
403        };
404        Self {
405            slot0: CommitSlotDiagnostic::from_slot(store.slot0()),
406            slot1: CommitSlotDiagnostic::from_slot(store.slot1()),
407            authoritative_generation,
408            recovery_error,
409        }
410    }
411}
412
413///
414/// CommitSlotDiagnostic
415///
416/// Read-only diagnostic summary for one protected commit slot.
417#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
418#[serde(deny_unknown_fields)]
419pub struct CommitSlotDiagnostic {
420    /// Whether a physical slot record is present.
421    pub present: bool,
422    /// Generation encoded by the slot, if present.
423    pub generation: Option<u64>,
424    /// Whether marker and checksum validation succeeded.
425    pub valid: bool,
426}
427
428impl CommitSlotDiagnostic {
429    fn from_slot<T: ProtectedGenerationSlot>(slot: Option<&T>) -> Self {
430        match slot {
431            Some(record) => Self {
432                present: true,
433                generation: Some(record.generation()),
434                valid: record.validates(),
435            },
436            None => Self {
437                present: false,
438                generation: None,
439                valid: false,
440            },
441        }
442    }
443}
444
445///
446/// CommitRecoveryError
447///
448/// Protected commit recovery failure.
449#[non_exhaustive]
450#[derive(Clone, Copy, Debug, Deserialize, Eq, thiserror::Error, PartialEq, Serialize)]
451pub enum CommitRecoveryError {
452    /// No committed slot passed marker and checksum validation.
453    #[error("no valid committed ledger generation")]
454    NoValidGeneration,
455    /// Both physical slots validated at the same generation but contained different bytes.
456    #[error("ambiguous committed ledger generation {generation}")]
457    AmbiguousGeneration {
458        /// Ambiguous physical generation.
459        generation: u64,
460    },
461    /// Physical generation advancement would overflow.
462    #[error("committed ledger generation {generation} cannot be advanced without overflow")]
463    GenerationOverflow {
464        /// Last valid physical generation.
465        generation: u64,
466    },
467    /// Caller attempted to commit a physical generation other than the next generation.
468    #[error("expected committed ledger generation {expected}, got {actual}")]
469    UnexpectedGeneration {
470        /// Expected next physical generation.
471        expected: u64,
472        /// Actual requested physical generation.
473        actual: u64,
474    },
475}
476
477fn generation_checksum(generation: &CommittedGenerationBytes) -> u64 {
478    let mut hash = FNV_OFFSET;
479    hash = hash_u64(hash, generation.generation);
480    hash = hash_u64(hash, generation.commit_marker);
481    hash = hash_usize(hash, generation.payload.len());
482    for byte in &generation.payload {
483        hash = hash_byte(hash, *byte);
484    }
485    hash
486}
487
488fn hash_usize(hash: u64, value: usize) -> u64 {
489    hash_u64(hash, value as u64)
490}
491
492fn hash_u64(mut hash: u64, value: u64) -> u64 {
493    for byte in value.to_le_bytes() {
494        hash = hash_byte(hash, byte);
495    }
496    hash
497}
498
499const fn hash_byte(hash: u64, byte: u8) -> u64 {
500    (hash ^ byte as u64).wrapping_mul(FNV_PRIME)
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    fn payload(value: u8) -> Vec<u8> {
508        vec![value; 4]
509    }
510
511    #[test]
512    fn committed_generation_validates_marker_and_checksum() {
513        let mut generation = CommittedGenerationBytes::new(7, payload(1));
514        assert!(generation.validates());
515
516        generation.checksum = generation.checksum.wrapping_add(1);
517        assert!(!generation.validates());
518    }
519
520    #[test]
521    fn physical_commit_accessors_expose_read_only_state() {
522        let mut store = DualCommitStore::default();
523        store.commit_payload(payload(1)).expect("first commit");
524
525        let slot = store.slot0().expect("first slot");
526
527        assert_eq!(slot.generation(), 0);
528        assert_eq!(slot.payload(), payload(1).as_slice());
529        assert_eq!(slot.commit_marker(), COMMIT_MARKER);
530        assert_eq!(slot.checksum(), generation_checksum(slot));
531        assert!(store.slot1().is_none());
532    }
533
534    #[test]
535    fn authoritative_selects_highest_valid_generation() {
536        let mut store = DualCommitStore::default();
537        store.commit_payload(payload(1)).expect("first commit");
538        store.commit_payload(payload(2)).expect("second commit");
539
540        let authoritative = store.authoritative().expect("authoritative");
541        let authoritative_slot =
542            select_authoritative_slot(store.slot0.as_ref(), store.slot1.as_ref())
543                .expect("authoritative slot");
544
545        assert_eq!(authoritative.generation, 1);
546        assert_eq!(authoritative.payload, payload(2));
547        assert_eq!(authoritative_slot.index, CommitSlotIndex::Slot1);
548        assert_eq!(authoritative_slot.record.payload, payload(2));
549    }
550
551    #[test]
552    fn corrupt_newer_slot_leaves_prior_generation_authoritative() {
553        let mut store = DualCommitStore::default();
554        store.commit_payload(payload(1)).expect("first commit");
555        store.write_corrupt_inactive_slot(1, payload(2));
556
557        let authoritative = store.authoritative().expect("authoritative");
558
559        assert_eq!(authoritative.generation, 0);
560        assert_eq!(authoritative.payload, payload(1));
561    }
562
563    #[test]
564    fn no_valid_generation_fails_closed() {
565        let mut store = DualCommitStore::default();
566        store.write_corrupt_inactive_slot(0, payload(1));
567        store.write_corrupt_inactive_slot(1, payload(2));
568
569        let err = store.authoritative().expect_err("no valid slot");
570
571        assert_eq!(err, CommitRecoveryError::NoValidGeneration);
572    }
573
574    #[test]
575    fn same_generation_identical_slots_recover_deterministically() {
576        let committed = CommittedGenerationBytes::new(7, payload(1));
577        let store = DualCommitStore {
578            slot0: Some(committed.clone()),
579            slot1: Some(committed),
580        };
581
582        let authoritative = store.authoritative_slot().expect("authoritative");
583
584        assert_eq!(authoritative.index, CommitSlotIndex::Slot0);
585        assert_eq!(authoritative.record.generation, 7);
586    }
587
588    #[test]
589    fn same_generation_divergent_slots_fail_closed() {
590        let store = DualCommitStore {
591            slot0: Some(CommittedGenerationBytes::new(7, payload(1))),
592            slot1: Some(CommittedGenerationBytes::new(7, payload(2))),
593        };
594
595        let err = store.authoritative().expect_err("ambiguous generation");
596
597        assert_eq!(
598            err,
599            CommitRecoveryError::AmbiguousGeneration { generation: 7 }
600        );
601    }
602
603    #[test]
604    fn physical_generation_overflow_fails_closed() {
605        let mut store = DualCommitStore {
606            slot0: Some(CommittedGenerationBytes::new(u64::MAX, payload(1))),
607            slot1: None,
608        };
609
610        let err = store
611            .commit_payload(payload(2))
612            .expect_err("overflow must fail");
613
614        assert_eq!(
615            err,
616            CommitRecoveryError::GenerationOverflow {
617                generation: u64::MAX
618            }
619        );
620    }
621
622    #[test]
623    fn diagnostic_reports_authoritative_generation_and_corrupt_slots() {
624        let mut store = DualCommitStore::default();
625        store.commit_payload(payload(1)).expect("first commit");
626        store.write_corrupt_inactive_slot(1, payload(2));
627
628        let diagnostic = store.diagnostic();
629
630        assert_eq!(diagnostic.authoritative_generation, Some(0));
631        assert_eq!(diagnostic.recovery_error, None);
632        assert_eq!(diagnostic.slot0.generation, Some(0));
633        assert!(diagnostic.slot0.valid);
634        assert_eq!(diagnostic.slot1.generation, Some(1));
635        assert!(!diagnostic.slot1.valid);
636    }
637
638    #[test]
639    fn diagnostic_reports_no_valid_generation_for_empty_store() {
640        let diagnostic = DualCommitStore::default().diagnostic();
641
642        assert_eq!(diagnostic.authoritative_generation, None);
643        assert_eq!(
644            diagnostic.recovery_error,
645            Some(CommitRecoveryError::NoValidGeneration)
646        );
647        assert!(!diagnostic.slot0.present);
648        assert!(!diagnostic.slot1.present);
649    }
650
651    #[test]
652    fn diagnostic_builds_from_any_dual_protected_store() {
653        #[derive(Eq, PartialEq)]
654        struct TestSlot {
655            generation: u64,
656            valid: bool,
657        }
658
659        impl ProtectedGenerationSlot for TestSlot {
660            fn generation(&self) -> u64 {
661                self.generation
662            }
663
664            fn validates(&self) -> bool {
665                self.valid
666            }
667        }
668
669        struct TestStore {
670            slot0: Option<TestSlot>,
671            slot1: Option<TestSlot>,
672        }
673
674        impl DualProtectedCommitStore for TestStore {
675            type Slot = TestSlot;
676
677            fn slot0(&self) -> Option<&Self::Slot> {
678                self.slot0.as_ref()
679            }
680
681            fn slot1(&self) -> Option<&Self::Slot> {
682                self.slot1.as_ref()
683            }
684        }
685
686        let diagnostic = CommitStoreDiagnostic::from_store(&TestStore {
687            slot0: Some(TestSlot {
688                generation: 8,
689                valid: true,
690            }),
691            slot1: Some(TestSlot {
692                generation: 9,
693                valid: false,
694            }),
695        });
696
697        assert_eq!(diagnostic.authoritative_generation, Some(8));
698        assert!(diagnostic.slot0.valid);
699        assert_eq!(diagnostic.slot1.generation, Some(9));
700        assert!(!diagnostic.slot1.valid);
701    }
702
703    #[test]
704    fn uninitialized_distinguishes_empty_from_corrupt() {
705        let mut store = DualCommitStore::default();
706        assert!(store.is_uninitialized());
707
708        store.write_corrupt_inactive_slot(0, payload(1));
709
710        assert!(!store.is_uninitialized());
711    }
712
713    #[test]
714    fn commit_after_corrupt_slot_advances_from_prior_valid_generation() {
715        let mut store = DualCommitStore::default();
716        store.commit_payload(payload(1)).expect("first commit");
717        store.write_corrupt_inactive_slot(1, payload(2));
718        store.commit_payload(payload(3)).expect("third commit");
719
720        let authoritative = store.authoritative().expect("authoritative");
721
722        assert_eq!(authoritative.generation, 1);
723        assert_eq!(authoritative.payload, payload(3));
724    }
725}