Skip to main content

canic_core/model/intent/
mod.rs

1//! Module: model::intent
2//!
3//! Responsibility: define pure receipt-backed intent identity, state, and operation results.
4//! Does not own: stable storage, domain receipt validation, or external-effect execution.
5//! Boundary: consumers validate domain evidence before constructing terminal evidence.
6
7use crate::{cdk::types::Principal, ids::IntentResourceKey, model::replay::OperationId};
8use serde::{Deserialize, Serialize};
9
10pub const PAYLOAD_BINDING_SCHEMA_VERSION: u32 = 1;
11pub const RECEIPT_BACKED_INTENT_SCHEMA_VERSION: u32 = 1;
12pub const TERMINAL_EVIDENCE_SCHEMA_VERSION: u32 = 1;
13pub const CANIC_INTENT_RESOURCE_PREFIX: &str = "canic:";
14pub const MAX_RECEIPT_BACKED_INTENT_REPLAY_WINDOW_NS: u64 = 24 * 60 * 60 * 1_000_000_000;
15pub const RECEIPT_TERMINAL_OBSERVATION_GRACE_NS: u64 = MAX_RECEIPT_BACKED_INTENT_REPLAY_WINDOW_NS;
16
17/// Pure temporal admission decision supplied by the workflow policy boundary.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum ReceiptReplayWindowDecision {
20    Open,
21    Closed,
22    TooLong { remaining_ns: u64 },
23}
24
25/// Derive the exact safe deletion deadline for one terminal application receipt.
26#[must_use]
27pub const fn receipt_terminal_eligible_at(
28    replay_deadline_ns: u64,
29    terminal_timestamp_ns: u64,
30) -> Option<u64> {
31    let Some(observation_deadline_ns) =
32        terminal_timestamp_ns.checked_add(RECEIPT_TERMINAL_OBSERVATION_GRACE_NS)
33    else {
34        return None;
35    };
36    Some(if replay_deadline_ns > observation_deadline_ns {
37        replay_deadline_ns
38    } else {
39        observation_deadline_ns
40    })
41}
42
43#[must_use]
44pub fn is_canic_owned_intent_resource_key(resource_key: &IntentResourceKey) -> bool {
45    resource_key.starts_with(CANIC_INTENT_RESOURCE_PREFIX)
46}
47
48/// Opaque, versioned binding for every field that changes an external effect.
49#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
50pub struct PayloadBinding {
51    pub schema_version: u32,
52    pub digest: [u8; 32],
53}
54
55impl PayloadBinding {
56    #[must_use]
57    pub const fn new(digest: [u8; 32]) -> Self {
58        Self {
59            schema_version: PAYLOAD_BINDING_SCHEMA_VERSION,
60            digest,
61        }
62    }
63}
64
65/// Consumer-validated terminal decision for one receipt-backed reservation.
66#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
67pub enum TerminalEvidenceDecision {
68    Committed,
69    RolledBack,
70}
71
72/// Bounded proof reference validated by the domain owner before settlement.
73#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
74pub struct TerminalEvidence {
75    pub source_canister: Principal,
76    pub schema_version: u32,
77    pub decision: TerminalEvidenceDecision,
78    pub fingerprint: [u8; 32],
79}
80
81impl TerminalEvidence {
82    #[must_use]
83    pub const fn new(
84        source_canister: Principal,
85        decision: TerminalEvidenceDecision,
86        fingerprint: [u8; 32],
87    ) -> Self {
88        Self {
89            source_canister,
90            schema_version: TERMINAL_EVIDENCE_SCHEMA_VERSION,
91            decision,
92            fingerprint,
93        }
94    }
95}
96
97/// Durable lifecycle of one receipt-backed intent.
98#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
99pub enum ReceiptBackedIntentState {
100    Pending,
101    Committed { evidence: TerminalEvidence },
102    RolledBack { evidence: TerminalEvidence },
103}
104
105/// Read-only projection of one receipt-backed intent.
106#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
107pub struct ReceiptBackedIntent {
108    pub schema_version: u32,
109    pub operation_id: OperationId,
110    pub payload_binding: PayloadBinding,
111    pub resource_key: IntentResourceKey,
112    pub quantity: u64,
113    pub state: ReceiptBackedIntentState,
114    pub revision: u64,
115    pub created_at_ns: u64,
116    pub updated_at_ns: u64,
117}
118
119/// Complete input for beginning one locally decidable, expirable intent.
120#[derive(Clone, Debug, Eq, PartialEq)]
121pub struct BeginLocalIntentInput {
122    pub resource_key: IntentResourceKey,
123    pub quantity: u64,
124    pub ttl_secs: Option<u64>,
125    pub reservation_limit: Option<u64>,
126}
127
128/// Complete input for idempotently beginning or loading one reservation.
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct BeginReceiptBackedIntentInput {
131    pub operation_id: OperationId,
132    pub payload_binding: PayloadBinding,
133    pub resource_key: IntentResourceKey,
134    pub quantity: u64,
135    pub reservation_limit: u64,
136    pub replay_deadline_ns: u64,
137}
138
139/// Internal placement admission that deliberately has no application replay deadline.
140#[derive(Clone, Debug, Eq, PartialEq)]
141pub struct BeginPlacementReceiptBackedIntentInput {
142    pub operation_id: OperationId,
143    pub payload_binding: PayloadBinding,
144    pub resource_key: IntentResourceKey,
145    pub quantity: u64,
146    pub reservation_limit: u64,
147}
148
149/// Idempotent outcome of receipt-backed begin-or-load.
150#[derive(Clone, Debug, Eq, PartialEq)]
151pub enum BeginReceiptBackedIntentResult {
152    Created {
153        revision: u64,
154    },
155    ExistingPending {
156        revision: u64,
157    },
158    ExistingCommitted {
159        revision: u64,
160        evidence: TerminalEvidence,
161    },
162    ExistingRolledBack {
163        revision: u64,
164        evidence: TerminalEvidence,
165    },
166    BindingConflict,
167    ReplayWindowClosed {
168        replay_deadline_ns: u64,
169    },
170    ReplayWindowTooLong {
171        remaining_ns: u64,
172        maximum_ns: u64,
173    },
174    CapacityExceeded {
175        current_quantity: u64,
176        requested_quantity: u64,
177        limit: u64,
178    },
179    StoreCapacityReached {
180        current_records: u64,
181        limit: u64,
182    },
183}
184
185/// Complete input for compare-and-set terminal settlement.
186#[derive(Clone, Debug, Eq, PartialEq)]
187pub struct SettleReceiptBackedIntentInput {
188    pub operation_id: OperationId,
189    pub expected_revision: u64,
190    pub expected_payload_binding: PayloadBinding,
191    pub evidence: TerminalEvidence,
192}
193
194/// Outcome of receipt-backed compare-and-set settlement.
195#[derive(Clone, Copy, Debug, Eq, PartialEq)]
196pub enum SettleReceiptBackedIntentResult {
197    Settled {
198        revision: u64,
199        state: ReceiptBackedIntentState,
200    },
201    AlreadySettled {
202        revision: u64,
203        state: ReceiptBackedIntentState,
204    },
205    NotFound,
206    RevisionConflict {
207        actual_revision: u64,
208    },
209    BindingConflict,
210}
211
212/// Complete input for deleting terminal evidence after its external receipt is released.
213#[derive(Clone, Debug, Eq, PartialEq)]
214pub struct RemoveTerminalReceiptBackedIntentInput {
215    pub operation_id: OperationId,
216    pub expected_revision: u64,
217    pub expected_payload_binding: PayloadBinding,
218}
219
220/// Outcome of deleting one exact terminal receipt-backed intent.
221#[derive(Clone, Copy, Debug, Eq, PartialEq)]
222pub enum RemoveTerminalReceiptBackedIntentResult {
223    Removed,
224    NotFound,
225    NotTerminal,
226    RevisionConflict { actual_revision: u64 },
227    BindingConflict,
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn terminal_eligibility_preserves_both_retention_deadlines() {
236        assert_eq!(
237            receipt_terminal_eligible_at(100, 50),
238            Some(50 + RECEIPT_TERMINAL_OBSERVATION_GRACE_NS)
239        );
240        assert_eq!(
241            receipt_terminal_eligible_at(u64::MAX - 1, 50),
242            Some(u64::MAX - 1)
243        );
244        assert_eq!(receipt_terminal_eligible_at(u64::MAX, u64::MAX), None);
245    }
246}