Skip to main content

prikk_object/payload/
block.rs

1//! Block payload types.
2
3use prikk_error::{PrikkError, Result};
4
5use crate::canonical::is_strictly_sorted;
6use crate::payload::common::MerkleRoot;
7use crate::{CanonicalEncode, CanonicalWriter, ObjectId};
8
9/// Block kind.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11#[repr(u16)]
12pub enum BlockKind {
13    /// Root block.
14    Root = 1,
15    /// Normal block.
16    Normal = 2,
17    /// Merge block.
18    Merge = 3,
19    /// Repair block.
20    Repair = 4,
21    /// Import block.
22    Import = 5,
23}
24
25impl BlockKind {
26    /// Stable code.
27    #[must_use]
28    pub const fn code(self) -> u16 {
29        self as u16
30    }
31
32    /// Parse a stable block-kind code.
33    pub fn from_code(code: u32) -> Result<Self> {
34        match code {
35            1 => Ok(Self::Root),
36            2 => Ok(Self::Normal),
37            3 => Ok(Self::Merge),
38            4 => Ok(Self::Repair),
39            5 => Ok(Self::Import),
40            other => Err(PrikkError::MalformedData(format!(
41                "unknown block kind code: {other}"
42            ))),
43        }
44    }
45}
46
47/// Block payload. Block summaries are intentionally not identity-bearing.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct BlockPayload {
50    /// Parent block IDs, sorted unless a later design adds semantic parent roles.
51    pub parent_block_ids: Vec<ObjectId>,
52    /// Block kind.
53    pub kind: BlockKind,
54    /// Patch IDs in canonical block patch order.
55    pub patch_ids: Vec<ObjectId>,
56    /// State Merkle root.
57    pub state_merkle_root: MerkleRoot,
58    /// Optional full snapshot blob reference.
59    pub snapshot_blob_ref: Option<ObjectId>,
60    /// The parent state derivation and replay follow. Present only on `Merge` blocks (DC-75); must
61    /// name one of `parent_block_ids`. `None` for every other kind, which have at most one parent
62    /// already and need no designation.
63    pub mainline_parent_id: Option<ObjectId>,
64    /// The block confluence was proven against when this `Merge` was sealed (DC-75). A claim, not a
65    /// trust boundary: `verify` independently re-derives the true merge base and reports disagreement
66    /// rather than trusting this field. `None` for every other kind.
67    pub merge_baseline_block_id: Option<ObjectId>,
68}
69
70impl BlockPayload {
71    /// Decode a block payload from Prikk canonical TLV bytes.
72    pub fn decode_canonical(bytes: &[u8]) -> Result<Self> {
73        let mut cursor = BlockCanonicalCursor::new(bytes);
74        let mut parent_block_ids = Vec::new();
75        let mut kind = None;
76        let mut patch_ids = Vec::new();
77        let mut state_merkle_root = None;
78        let mut snapshot_blob_ref = None;
79        let mut mainline_parent_id = None;
80        let mut merge_baseline_block_id = None;
81        while let Some(field) = cursor.next_field()? {
82            match field.tag {
83                1 => parent_block_ids.push(field.read_object_id()?),
84                2 => kind = Some(BlockKind::from_code(u32::from(field.read_enum_u16()?))?),
85                3 => patch_ids.push(field.read_object_id()?),
86                4 => state_merkle_root = Some(MerkleRoot(field.read_array::<32>()?)),
87                5 => snapshot_blob_ref = Some(field.read_object_id()?),
88                6 => mainline_parent_id = Some(field.read_object_id()?),
89                7 => merge_baseline_block_id = Some(field.read_object_id()?),
90                other => {
91                    return Err(PrikkError::MalformedData(format!(
92                        "unknown Block field tag: {other}"
93                    )));
94                }
95            }
96        }
97        let payload = Self {
98            parent_block_ids,
99            kind: kind
100                .ok_or_else(|| PrikkError::MalformedData("Block missing kind".to_string()))?,
101            patch_ids,
102            state_merkle_root: state_merkle_root.ok_or_else(|| {
103                PrikkError::MalformedData("Block missing state_merkle_root".to_string())
104            })?,
105            snapshot_blob_ref,
106            mainline_parent_id,
107            merge_baseline_block_id,
108        };
109        if !is_strictly_sorted(&payload.parent_block_ids) {
110            return Err(PrikkError::MalformedData(
111                "Block parent IDs are not sorted and unique".to_string(),
112            ));
113        }
114        Ok(payload)
115    }
116}
117
118struct BlockCanonicalCursor<'a> {
119    bytes: &'a [u8],
120    pos: usize,
121    last_tag: Option<u16>,
122}
123
124impl<'a> BlockCanonicalCursor<'a> {
125    const fn new(bytes: &'a [u8]) -> Self {
126        Self {
127            bytes,
128            pos: 0,
129            last_tag: None,
130        }
131    }
132
133    fn next_field(&mut self) -> Result<Option<BlockCanonicalField<'a>>> {
134        if self.pos == self.bytes.len() {
135            return Ok(None);
136        }
137        let tag = u16::from_be_bytes(self.read_array::<2>()?);
138        if tag == 0 {
139            return Err(PrikkError::MalformedData(
140                "field tag 0 is reserved".to_string(),
141            ));
142        }
143        if let Some(last) = self.last_tag {
144            if tag < last {
145                return Err(PrikkError::MalformedData(format!(
146                    "field tag order violation: {tag} after {last}"
147                )));
148            }
149        }
150        self.last_tag = Some(tag);
151        let wire_type = self.read_u8()?;
152        let len = usize::try_from(u64::from_be_bytes(self.read_array::<8>()?)).map_err(|_| {
153            PrikkError::MalformedData("canonical field length does not fit usize".to_string())
154        })?;
155        let value = self.read_exact(len)?;
156        Ok(Some(BlockCanonicalField {
157            tag,
158            wire_type,
159            value,
160        }))
161    }
162
163    fn read_u8(&mut self) -> Result<u8> {
164        let value = self.read_exact(1)?;
165        let Some(byte) = value.first() else {
166            return Err(PrikkError::MalformedData(
167                "unexpected empty byte".to_string(),
168            ));
169        };
170        Ok(*byte)
171    }
172
173    fn read_array<const N: usize>(&mut self) -> Result<[u8; N]> {
174        let bytes = self.read_exact(N)?;
175        let mut out = [0_u8; N];
176        out.copy_from_slice(bytes);
177        Ok(out)
178    }
179
180    fn read_exact(&mut self, len: usize) -> Result<&'a [u8]> {
181        let end = self
182            .pos
183            .checked_add(len)
184            .ok_or_else(|| PrikkError::MalformedData("canonical range overflow".to_string()))?;
185        let Some(slice) = self.bytes.get(self.pos..end) else {
186            return Err(PrikkError::MalformedData(
187                "unexpected end of canonical payload".to_string(),
188            ));
189        };
190        self.pos = end;
191        Ok(slice)
192    }
193}
194
195struct BlockCanonicalField<'a> {
196    tag: u16,
197    wire_type: u8,
198    value: &'a [u8],
199}
200
201impl<'a> BlockCanonicalField<'a> {
202    fn read_object_id(&self) -> Result<ObjectId> {
203        self.require_wire(crate::canonical::WireType::ObjectId)?;
204        Ok(ObjectId::from_bytes(self.read_array::<32>()?))
205    }
206
207    fn read_enum_u16(&self) -> Result<u16> {
208        self.require_wire(crate::canonical::WireType::EnumU16)?;
209        Ok(u16::from_be_bytes(self.read_array::<2>()?))
210    }
211
212    fn require_wire(&self, expected: crate::canonical::WireType) -> Result<()> {
213        if self.wire_type == expected as u8 {
214            return Ok(());
215        }
216        Err(PrikkError::MalformedData(format!(
217            "field {} has wrong wire type: expected {}, got {}",
218            self.tag, expected as u8, self.wire_type
219        )))
220    }
221
222    fn read_array<const N: usize>(&self) -> Result<[u8; N]> {
223        if self.value.len() != N {
224            return Err(PrikkError::MalformedData(format!(
225                "field {} expected {N} bytes, got {}",
226                self.tag,
227                self.value.len()
228            )));
229        }
230        let mut out = [0_u8; N];
231        out.copy_from_slice(self.value);
232        Ok(out)
233    }
234}
235
236impl CanonicalEncode for BlockPayload {
237    fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
238        if !is_strictly_sorted(&self.parent_block_ids) {
239            return Err(PrikkError::CanonicalEncoding(
240                "parent_block_ids must be sorted and unique".to_string(),
241            ));
242        }
243        writer.repeated_object_id(1, &self.parent_block_ids)?;
244        writer.field_enum_u16(2, self.kind.code())?;
245        writer.repeated_object_id(3, &self.patch_ids)?;
246        writer.field_bytes(4, &self.state_merkle_root.0)?;
247        if let Some(snapshot) = self.snapshot_blob_ref {
248            writer.field_object_id(5, &snapshot)?;
249        }
250        if let Some(mainline) = self.mainline_parent_id {
251            writer.field_object_id(6, &mainline)?;
252        }
253        if let Some(baseline) = self.merge_baseline_block_id {
254            writer.field_object_id(7, &baseline)?;
255        }
256        Ok(())
257    }
258}