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