Skip to main content

coven_format/
sequence.rs

1//! Plaintext slot headers and snapshot announcements (D9).
2
3use crate::admin::{SnapshotId, StoreChange};
4use crate::error::{require, Error, Rule};
5use crate::path::ObjectPath;
6use crate::value::{positive, Slot};
7use crate::wire::{decode_frame, decode_frame_value, wire_struct, Decoder, Encoder, Wire};
8use coven_foundation::id_source::{DeviceId, MemberId};
9
10/// Complete limit for an admin, key-copy or announcement slot (D2).
11pub const MAX_BOUNDED_SLOT: usize = 4 * 1024 * 1024;
12
13/// The author and claim identity sealed under the store key in every slot.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct SlotHeader {
16    /// The claiming device.
17    pub device: DeviceId,
18    /// Positive number from this device's counter across every slot kind.
19    pub claim: u64,
20    /// The member whose credential signs the slot.
21    pub author: MemberId,
22    /// The immediately preceding slot, zero for creation.
23    pub after: u64,
24}
25wire_struct!(SlotHeader, device, claim, author, after);
26impl SlotHeader {
27    /// The encoded frame's length: its prefix, then device, claim, author
28    /// and predecessor.
29    pub const ENCODED_LEN: usize = crate::FRAME_PREFIX_LEN + 8 + 8 + 16 + 8;
30
31    /// Encode the kind-14 frame, checking its path binding.
32    pub fn encode(&self, path: &ObjectPath) -> Result<Vec<u8>, Error> {
33        self.validate_at(path)?;
34        crate::encode_frame(crate::kinds::SLOT_HEADER, self)
35    }
36
37    /// Decode exactly the kind-14 frame and check its path binding.
38    pub fn decode(bytes: &[u8], path: &ObjectPath) -> Result<Self, Error> {
39        let header: Self = decode_frame_value(bytes, crate::kinds::SLOT_HEADER, "slot header")?;
40        header.validate_at(path)?;
41        Ok(header)
42    }
43
44    /// A header's predecessor must be exactly one less than its path's slot.
45    pub fn validate_at(&self, path: &ObjectPath) -> Result<(), Error> {
46        positive(self.claim)?;
47        require(
48            path.slot_number().is_some_and(|slot| slot.get() - 1 == self.after),
49            "slot predecessor",
50            Rule::Coverage,
51        )
52    }
53
54    /// Check an opened inner write against its containing slot. The consumer
55    /// resolves its historical signer and its source writes separately.
56    pub fn check_write(&self, write: &crate::write::WriteHeader) -> Result<(), Error> {
57        write.validate()?;
58        require(write.position.device == self.device, "slot write device", Rule::WriteDevice)?;
59        require(write.read_through <= self.after, "slot write read-through", Rule::Coverage)
60    }
61}
62
63/// The state a snapshot supplies; preparations require an activating admin slot.
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum Purpose {
66    /// State at the existing schema and reset.
67    Ordinary,
68    /// Prepared state at a positive new app schema version.
69    Raise(u32),
70    /// Prepared replacement state for a reset.
71    Reset,
72}
73impl Purpose {
74    pub(crate) fn validate(&self) -> Result<(), Error> {
75        if let Self::Raise(version) = self {
76            require(*version > 0, "snapshot raise version", Rule::RaiseVersion)?;
77        }
78        Ok(())
79    }
80}
81impl Wire for Purpose {
82    fn put(&self, out: &mut Encoder) -> Result<(), Error> {
83        self.validate()?;
84        match self {
85            Self::Ordinary => 0u8.put(out),
86            Self::Raise(version) => {
87                1u8.put(out)?;
88                version.put(out)
89            }
90            Self::Reset => 2u8.put(out),
91        }
92    }
93    fn get(input: &mut Decoder<'_>) -> Result<Self, Error> {
94        let purpose = match u8::get(input)? {
95            0 => Self::Ordinary,
96            1 => Self::Raise(u32::get(input)?),
97            2 => Self::Reset,
98            tag => return Err(Error::UnknownTag { field: "snapshot purpose", tag }),
99        };
100        purpose.validate()?;
101        Ok(purpose)
102    }
103}
104
105/// A claimed announcement of an immutable snapshot body.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct SnapshotAnnouncement {
108    /// Audience, writer and object number forming the snapshot path.
109    pub snapshot: SnapshotId,
110    /// Last included slot; strictly below the announcing slot.
111    pub covers: u64,
112    /// The version of the newest breaking migration the body went through,
113    /// zero if none. Applying the announcement refuses one its audience has
114    /// not been raised to by the coverage (ยง16.3).
115    pub breaking_version: u32,
116    /// Ordinary state or an admin operation's preparation.
117    pub purpose: Purpose,
118    /// SHA-256 of the complete sealed snapshot object.
119    pub digest: [u8; 32],
120}
121wire_struct!(SnapshotAnnouncement, snapshot, covers, breaking_version, purpose, digest);
122impl SnapshotAnnouncement {
123    /// Encode the announcement at its claimed slot.
124    pub fn encode(&self, slot: Slot) -> Result<Vec<u8>, Error> {
125        self.validate_at(slot)?;
126        crate::encode_frame(crate::kinds::SNAPSHOT_ANNOUNCEMENT, self)
127    }
128    /// Decode exactly one announcement and check coverage precedes its slot.
129    pub fn decode(bytes: &[u8], slot: Slot) -> Result<Self, Error> {
130        let announcement: Self = decode_frame_value(
131            bytes,
132            crate::kinds::SNAPSHOT_ANNOUNCEMENT,
133            "snapshot announcement",
134        )?;
135        announcement.validate_at(slot)?;
136        Ok(announcement)
137    }
138    fn validate_at(&self, slot: Slot) -> Result<(), Error> {
139        positive(self.snapshot.number)?;
140        require(self.covers < slot.get(), "announcement coverage", Rule::Coverage)?;
141        self.purpose.validate()
142    }
143}
144
145/// The second frame of a bounded slot. Writes use a streamed body instead.
146#[derive(Clone, Debug, PartialEq, Eq)]
147pub enum SlotFrame {
148    /// The admin operation.
149    Admin(StoreChange),
150    /// Acknowledges the key box in the clear prefix.
151    KeyCopy,
152    /// Announces a snapshot body uploaded before the claim.
153    Announcement(SnapshotAnnouncement),
154}
155impl SlotFrame {
156    /// Encode both plaintext frames, checking path-dependent facts.
157    pub fn encode(&self, header: &SlotHeader, path: &ObjectPath) -> Result<Vec<u8>, Error> {
158        let mut bytes = header.encode(path)?;
159        let slot = path.slot_number().expect("header checked slot path");
160        let frame = match self {
161            Self::Admin(change) => {
162                check_admin(change, header, path)?;
163                change.encode()?
164            }
165            Self::KeyCopy => crate::encode_frame_with(crate::kinds::KEY_COPY_ACK, |_| Ok(()))?,
166            Self::Announcement(announcement) => announcement.encode(slot)?,
167        };
168        bytes.extend_from_slice(&frame);
169        Ok(bytes)
170    }
171
172    /// Decode both frames. The clear prefix's kind must also match this result.
173    pub fn decode(bytes: &[u8], path: &ObjectPath) -> Result<(SlotHeader, Self), Error> {
174        let length = crate::frame_length(bytes)?;
175        let header = SlotHeader::decode(bytes.get(..length).ok_or(Error::Truncated)?, path)?;
176        let frame = bytes.get(length..).ok_or(Error::Truncated)?;
177        let (kind, input) = decode_frame(frame)?;
178        let body = match kind {
179            crate::kinds::ADMIN => {
180                let change = StoreChange::decode(frame)?;
181                check_admin(&change, &header, path)?;
182                Self::Admin(change)
183            }
184            crate::kinds::SNAPSHOT_ANNOUNCEMENT => Self::Announcement(
185                SnapshotAnnouncement::decode(frame, path.slot_number().expect("checked path"))?,
186            ),
187            crate::kinds::KEY_COPY_ACK => {
188                input.finish()?;
189                Self::KeyCopy
190            }
191            tag => return Err(Error::UnknownTag { field: "slot frame", tag }),
192        };
193        Ok((header, body))
194    }
195}
196
197fn check_admin(change: &StoreChange, header: &SlotHeader, path: &ObjectPath) -> Result<(), Error> {
198    match change {
199        StoreChange::CreateStore { store, .. } => {
200            require(header.after == 0 && *store == path.store(), "creation slot", Rule::Store)
201        }
202        StoreChange::AddDevice { replaces, .. } => {
203            require(*replaces != Some(header.device), "replacement device", Rule::DeviceReplacement)
204        }
205        StoreChange::AdmitMember { device, .. } | StoreChange::RecoverMember { device, .. } => {
206            require(*device != header.device, "admitted device", Rule::AdmittedDevice)
207        }
208        StoreChange::RaiseSchema { announcement, .. } | StoreChange::Reset { announcement } => {
209            require(announcement.get() <= header.after, "activation announcement", Rule::Coverage)
210        }
211        _ => Ok(()),
212    }
213}
214
215#[cfg(test)]
216#[path = "sequence_tests.rs"]
217mod tests;