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