Skip to main content

ic_backup/model/execution_settlement/
mod.rs

1//! Immutable exact original journal fingerprints; not a backup/restore terminal receipt.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord, attempt_journal::AttemptJournalRecord, bounded_sequence,
5    effect_graph::MAX_EFFECT_OPERATIONS,
6};
7use serde::{Deserialize, Deserializer, Serialize};
8use thiserror::Error;
9
10/// Maximum raw and canonical checkpoint bytes, including all 8,192 possible journal rows.
11pub const MAX_EXECUTION_SETTLEMENT_BYTES: u64 = 2 * 1024 * 1024;
12
13/// One exact chronological journal fingerprint under its opaque original operation identity.
14#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
15#[serde(deny_unknown_fields)]
16pub struct ExecutionSettlementJournalRecord {
17    operation_sequence: u64,
18    history: ArtifactChecksumRecord,
19}
20impl ExecutionSettlementJournalRecord {
21    /// Project original sequence and exact history, without granting receipt authenticity.
22    #[must_use]
23    pub fn from_journal(journal: &AttemptJournalRecord) -> Self {
24        Self {
25            operation_sequence: journal.authority().binding().operation_sequence(),
26            history: journal.digest(),
27        }
28    }
29    /// Read the original opaque operation identity.
30    #[must_use]
31    pub const fn operation_sequence(&self) -> u64 {
32        self.operation_sequence
33    }
34    /// Read full original authority/reservation/receipt history identity.
35    #[must_use]
36    pub const fn history(&self) -> &ArtifactChecksumRecord {
37        &self.history
38    }
39}
40
41/// Strict v1 immutable exact journal checkpoint, with no copied counters or completion flags.
42///
43/// Structural decoding authenticates no receipts. Current admission requires the full
44/// exact original journal set, causal Applied prerequisites and every operation Applied.
45/// This is local ledger settlement, not full backup/restore completion, application
46/// safety, command quiescence, fresh authority or fence/reference-release permission.
47#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
48#[serde(try_from = "SettlementFields")]
49pub struct ExecutionSettlementRecord {
50    version: u16,
51    plan_intent: ArtifactChecksumRecord,
52    journals: Vec<ExecutionSettlementJournalRecord>,
53}
54#[derive(Deserialize)]
55#[serde(deny_unknown_fields)]
56struct SettlementFields {
57    version: u16,
58    plan_intent: ArtifactChecksumRecord,
59    #[serde(deserialize_with = "bounded_journals")]
60    journals: Vec<ExecutionSettlementJournalRecord>,
61}
62impl TryFrom<SettlementFields> for ExecutionSettlementRecord {
63    type Error = ExecutionSettlementError;
64    fn try_from(fields: SettlementFields) -> Result<Self, Self::Error> {
65        if fields.version != 1 {
66            return Err(ExecutionSettlementError::UnsupportedVersion(fields.version));
67        }
68        Self::new(fields.plan_intent, fields.journals)
69    }
70}
71impl ExecutionSettlementRecord {
72    /// Declare bounded unique journal fingerprints in canonical sequence order.
73    ///
74    /// Declaration alone proves no journal presence or Applied outcome. Policy and
75    /// persistence independently admit original retained journals before publication/replay.
76    /// # Errors
77    /// Rejects empty/excessive rows or repeated operation identity, including different hashes.
78    pub fn new(
79        plan_intent: ArtifactChecksumRecord,
80        mut journals: Vec<ExecutionSettlementJournalRecord>,
81    ) -> Result<Self, ExecutionSettlementError> {
82        if journals.is_empty() || journals.len() > MAX_EFFECT_OPERATIONS {
83            return Err(ExecutionSettlementError::InvalidJournalCount);
84        }
85        journals.sort_by_key(ExecutionSettlementJournalRecord::operation_sequence);
86        if journals
87            .windows(2)
88            .any(|rows| rows[0].operation_sequence == rows[1].operation_sequence)
89        {
90            return Err(ExecutionSettlementError::DuplicateOperation);
91        }
92        Ok(Self {
93            version: 1,
94            plan_intent,
95            journals,
96        })
97    }
98    /// Read original full plan identity, including all original allowances.
99    #[must_use]
100    pub const fn plan_intent(&self) -> &ArtifactChecksumRecord {
101        &self.plan_intent
102    }
103    /// Read exact canonical fingerprint set; no row is a dispatch/completion receipt.
104    #[must_use]
105    pub fn journals(&self) -> &[ExecutionSettlementJournalRecord] {
106        &self.journals
107    }
108    /// Hash NUL-terminated v1 domain, 64 ASCII plan hash, u64 row count and
109    /// ascending u64 operation identity/64 ASCII history hash pairs, all integers big-endian.
110    #[must_use]
111    pub fn digest(&self) -> ArtifactChecksumRecord {
112        let mut bytes = b"ic-backup/execution-settlement/v1\0".to_vec();
113        bytes.extend_from_slice(self.plan_intent.hash().as_bytes());
114        bytes.extend_from_slice(&(self.journals.len() as u64).to_be_bytes());
115        for row in &self.journals {
116            bytes.extend_from_slice(&row.operation_sequence.to_be_bytes());
117            bytes.extend_from_slice(row.history.hash().as_bytes());
118        }
119        ArtifactChecksumRecord::from_bytes(&bytes)
120    }
121}
122fn bounded_journals<'de, D: Deserializer<'de>>(
123    deserializer: D,
124) -> Result<Vec<ExecutionSettlementJournalRecord>, D::Error> {
125    bounded_sequence::deserialize::<_, _, MAX_EFFECT_OPERATIONS>(
126        deserializer,
127        format!("at most {MAX_EFFECT_OPERATIONS} original journal fingerprints"),
128        "too many journal fingerprints",
129    )
130}
131/// Typed strict declaration rejection; original journals remain the only spending owners.
132#[derive(Debug, Eq, Error, PartialEq)]
133pub enum ExecutionSettlementError {
134    /// Historical or unknown generation cannot become a current checkpoint.
135    #[error("unsupported execution settlement version {0}")]
136    UnsupportedVersion(u16),
137    /// A checkpoint requires one through 8,192 unique original rows.
138    #[error("invalid execution settlement journal count")]
139    InvalidJournalCount,
140    /// Same operation was declared more than once.
141    #[error("duplicate execution settlement operation")]
142    DuplicateOperation,
143}
144#[cfg(test)]
145mod tests;