Skip to main content

chio_store_sqlite/
economic_state_cache.rs

1use std::sync::{Arc, Mutex, MutexGuard};
2
3use chio_core::canonical::canonical_json_bytes;
4use chio_core::economic_continuity::{
5    economic_effect_slot_from_head, verify_economic_completed_effect,
6    verify_economic_state_batch_commit, EconomicAdmissionHandoffStateV1, EconomicContentV1,
7    EconomicContinuityError, EconomicEffectSlotV1, EconomicEffectStateV1, EconomicEffectTerminalV1,
8    EconomicResourceHeadV1, EconomicResourceKeyV1, EconomicStateAnchorError,
9    EconomicStateAnchorPins, EconomicStateAnchorViewV1, EconomicStateBatchV1,
10    EconomicTerminalResultV1, VerifiedEconomicStateBatchAdvance, VerifiedEconomicStateView,
11    MAX_ECONOMIC_BATCH_BYTES,
12};
13use chio_core::{sha256_hex, StoreMutationFence};
14use chio_credit::clearing::CLEARING_LIFECYCLE_REPLAY_DESCRIPTOR_KIND;
15use chio_kernel::admission_operation::{
16    verify_economic_cancellation_terminal_advance, AdmissionOperationState, AdmissionOperationV1,
17    AdmissionProjectionRecordKind, AdmissionRecoveryLease, PersistedAdmissionOperationV1,
18    SignedAdmissionTerminalProjectionV1, VerifiedAdmissionTerminalProjectionV1,
19};
20use chio_kernel::ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND;
21use chio_settle::channel::{
22    CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY, CHANNEL_LIFECYCLE_RESOURCE_FAMILY,
23    CHANNEL_SERVICE_DISPATCH_EFFECT_KIND, CHANNEL_TRANSITION_REPLAY_FORMAT,
24};
25use rusqlite::{params, Connection, OptionalExtension, Row, Transaction, TransactionBehavior};
26use serde::de::DeserializeOwned;
27use serde::{Deserialize, Serialize};
28
29use crate::serving_owner::{SqliteServingOwner, SqliteServingOwnerError};
30
31mod persistence;
32pub(crate) use persistence::verify_cache_sql_invariants;
33use persistence::*;
34pub(crate) use persistence::{append_stage_commit, load_stage_tx, update_stage};
35
36const ECONOMIC_STATE_CACHE_SCHEMA_KEY: &str = "economic_state_cache";
37pub(crate) const ECONOMIC_STATE_CACHE_SUPPORTED_SCHEMA_VERSION: i32 = 3;
38const ECONOMIC_STATE_CACHE_SCHEMA_ANCHORS: &[&str] =
39    &["economic_state_stages", "capability_grant_budgets"];
40const MAX_REASON_BYTES: usize = 4 * 1024;
41const MAX_VIEW_BYTES: usize = 4 * MAX_ECONOMIC_BATCH_BYTES;
42const MAX_DESCRIPTOR_BYTES: usize = 4 * MAX_ECONOMIC_BATCH_BYTES;
43const MAX_DESCRIPTOR_KIND_BYTES: usize = 128;
44const MAX_DESCRIPTOR_KEY_BYTES: usize = 2_048;
45const MAX_TRUSTED_UNIX_MS: u64 = (1_u64 << 53) - 1;
46const GENESIS_STAGE_COMMIT_DIGEST: &str =
47    "0000000000000000000000000000000000000000000000000000000000000000";
48const ADMISSION_TERMINAL_EFFECT_RESULT_SCHEMA: &str = "chio.admission.terminal-effect-result.v1";
49
50const ECONOMIC_STATE_CACHE_SCHEMA: &str = include_str!("economic_state_cache.sql");
51const ECONOMIC_STATE_CACHE_DESCRIPTOR_MIGRATION: &str = r#"
52ALTER TABLE economic_state_stages ADD COLUMN descriptor_kind TEXT
53    CHECK (descriptor_kind IS NULL OR length(descriptor_kind) BETWEEN 1 AND 128);
54ALTER TABLE economic_state_stages ADD COLUMN descriptor_key TEXT
55    CHECK (descriptor_key IS NULL OR length(descriptor_key) BETWEEN 1 AND 2048);
56ALTER TABLE economic_state_stages ADD COLUMN descriptor_digest TEXT
57    CHECK (descriptor_digest IS NULL OR (
58        length(descriptor_digest) = 64
59        AND descriptor_digest NOT GLOB '*[^0-9a-f]*'
60    ));
61ALTER TABLE economic_state_stages ADD COLUMN descriptor_json BLOB
62    CHECK (descriptor_json IS NULL OR length(descriptor_json) BETWEEN 1 AND 4194304);
63DROP TRIGGER IF EXISTS economic_state_stage_identity_immutable;
64CREATE TRIGGER economic_state_stage_identity_immutable
65BEFORE UPDATE OF batch_id, checkpoint_sequence, checkpoint_digest,
66    base_view_json, batch_json, operation_binding_json, descriptor_kind,
67    descriptor_key, descriptor_digest, descriptor_json, created_at_unix_ms
68ON economic_state_stages
69BEGIN
70    SELECT RAISE(ABORT, 'economic stage identity is immutable');
71END;
72"#;
73
74#[derive(Debug, thiserror::Error)]
75pub enum EconomicStateCacheError {
76    #[error("economic state cache is unavailable: {0}")]
77    Unavailable(String),
78    #[error("economic state cache mutation was fenced")]
79    Fenced,
80    #[error("economic state stage was not found")]
81    NotFound,
82    #[error("economic state stage conflicts with retained state")]
83    Conflict,
84    #[error("economic state stage transition from {from:?} to {to:?} is invalid")]
85    InvalidTransition {
86        from: EconomicStateStageStatus,
87        to: EconomicStateStageStatus,
88    },
89    #[error("economic state cache invariant failed: {0}")]
90    Invariant(String),
91    #[error("economic state cache durable outcome is unknown: {0}")]
92    OutcomeUnknown(String),
93    #[error(transparent)]
94    Continuity(#[from] EconomicContinuityError),
95    #[error(transparent)]
96    Anchor(#[from] EconomicStateAnchorError),
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum EconomicStateStageStatus {
102    DbStaged,
103    EconomicAnchorAdvanced,
104    DbFinalized,
105    Discarded,
106    Quarantined,
107}
108
109impl EconomicStateStageStatus {
110    fn as_str(self) -> &'static str {
111        match self {
112            Self::DbStaged => "db_staged",
113            Self::EconomicAnchorAdvanced => "economic_anchor_advanced",
114            Self::DbFinalized => "db_finalized",
115            Self::Discarded => "discarded",
116            Self::Quarantined => "quarantined",
117        }
118    }
119
120    fn parse(value: &str) -> Result<Self, EconomicStateCacheError> {
121        match value {
122            "db_staged" => Ok(Self::DbStaged),
123            "economic_anchor_advanced" => Ok(Self::EconomicAnchorAdvanced),
124            "db_finalized" => Ok(Self::DbFinalized),
125            "discarded" => Ok(Self::Discarded),
126            "quarantined" => Ok(Self::Quarantined),
127            _ => Err(invariant("economic stage status is invalid")),
128        }
129    }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase", deny_unknown_fields)]
134pub struct EconomicOperationStageBinding {
135    operation_id: String,
136    operation_state: AdmissionOperationState,
137    operation_version: u64,
138    coordinator_lease_epoch: u64,
139    coordinator_lease_id: String,
140    recovery_claimant_id: String,
141    recovery_expires_at_unix_ms: u64,
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    not_after_unix_ms: Option<u64>,
144    store_fence: StoreMutationFence,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct EconomicStateStageDescriptor {
149    kind: String,
150    key: String,
151    digest: String,
152    canonical_json: Vec<u8>,
153}
154
155#[derive(Serialize)]
156#[serde(rename_all = "camelCase")]
157struct AdmissionTerminalEffectRecordCommitment {
158    kind: AdmissionProjectionRecordKind,
159    record_id: String,
160    record_digest: String,
161}
162
163#[derive(Serialize)]
164#[serde(rename_all = "camelCase")]
165struct AdmissionTerminalEffectCommitment {
166    schema: &'static str,
167    descriptor_digest: String,
168    operation_id: String,
169    request_id: String,
170    source_operation_digest: String,
171    terminal_operation_digest: String,
172    terminal_state: AdmissionOperationState,
173    projection_digest: String,
174    records: Vec<AdmissionTerminalEffectRecordCommitment>,
175}
176
177impl EconomicStateStageDescriptor {
178    pub fn new<T: Serialize>(
179        kind: impl Into<String>,
180        key: impl Into<String>,
181        value: &T,
182    ) -> Result<Self, EconomicStateCacheError> {
183        let descriptor = Self {
184            kind: kind.into(),
185            key: key.into(),
186            digest: String::new(),
187            canonical_json: canonical_bounded(value, MAX_DESCRIPTOR_BYTES, "stage descriptor")?,
188        };
189        descriptor.with_digest()
190    }
191
192    fn from_stored(
193        kind: String,
194        key: String,
195        digest: String,
196        canonical_json: Vec<u8>,
197    ) -> Result<Self, EconomicStateCacheError> {
198        let descriptor = Self {
199            kind,
200            key,
201            digest,
202            canonical_json,
203        };
204        descriptor.validate()?;
205        Ok(descriptor)
206    }
207
208    fn with_digest(mut self) -> Result<Self, EconomicStateCacheError> {
209        self.digest = sha256_hex(&self.canonical_json);
210        self.validate()?;
211        Ok(self)
212    }
213
214    fn validate(&self) -> Result<(), EconomicStateCacheError> {
215        validate_descriptor_text(
216            &self.kind,
217            MAX_DESCRIPTOR_KIND_BYTES,
218            "stage descriptor kind",
219        )?;
220        validate_descriptor_text(&self.key, MAX_DESCRIPTOR_KEY_BYTES, "stage descriptor key")?;
221        validate_digest(&self.digest, "stage descriptor digest")?;
222        if self.canonical_json.is_empty()
223            || self.canonical_json.len() > MAX_DESCRIPTOR_BYTES
224            || sha256_hex(&self.canonical_json) != self.digest
225        {
226            return Err(invariant("economic stage descriptor is invalid"));
227        }
228        decode_exact::<serde_json::Value>(&self.canonical_json, "stage descriptor")?;
229        Ok(())
230    }
231
232    #[must_use]
233    pub fn kind(&self) -> &str {
234        &self.kind
235    }
236
237    #[must_use]
238    pub fn key(&self) -> &str {
239        &self.key
240    }
241
242    #[must_use]
243    pub fn digest(&self) -> &str {
244        &self.digest
245    }
246
247    pub fn decode<T: DeserializeOwned + Serialize>(&self) -> Result<T, EconomicStateCacheError> {
248        self.validate()?;
249        decode_exact(&self.canonical_json, "stage descriptor")
250    }
251}
252
253impl EconomicOperationStageBinding {
254    fn validate(&self) -> Result<(), EconomicStateCacheError> {
255        if self.operation_id.is_empty()
256            || self.coordinator_lease_id.is_empty()
257            || self.recovery_claimant_id.is_empty()
258            || self.store_fence.store_uuid.is_empty()
259            || self.store_fence.lease_id.is_empty()
260        {
261            return Err(invariant("economic operation binding identity is invalid"));
262        }
263        if self.operation_version == 0 || self.operation_version > MAX_TRUSTED_UNIX_MS {
264            return Err(invariant(
265                "economic operation binding operation_version is invalid",
266            ));
267        }
268        if self.coordinator_lease_epoch == 0 || self.coordinator_lease_epoch > MAX_TRUSTED_UNIX_MS {
269            return Err(invariant(
270                "economic operation binding coordinator_lease_epoch is invalid",
271            ));
272        }
273        if self.recovery_expires_at_unix_ms == 0
274            || self.recovery_expires_at_unix_ms > MAX_TRUSTED_UNIX_MS
275        {
276            return Err(invariant(
277                "economic operation binding recovery_expires_at_unix_ms is invalid",
278            ));
279        }
280        if self
281            .not_after_unix_ms
282            .is_some_and(|value| value == 0 || value > MAX_TRUSTED_UNIX_MS)
283        {
284            return Err(invariant(
285                "economic operation binding not_after_unix_ms is invalid",
286            ));
287        }
288        if self.store_fence.owner_epoch == 0 || self.store_fence.owner_epoch > MAX_TRUSTED_UNIX_MS {
289            return Err(invariant(
290                "economic operation binding store fence is invalid",
291            ));
292        }
293        Ok(())
294    }
295
296    #[must_use]
297    pub fn operation_id(&self) -> &str {
298        &self.operation_id
299    }
300
301    #[must_use]
302    pub fn operation_state(&self) -> AdmissionOperationState {
303        self.operation_state
304    }
305
306    #[must_use]
307    pub fn operation_version(&self) -> u64 {
308        self.operation_version
309    }
310
311    #[must_use]
312    pub fn coordinator_lease_epoch(&self) -> u64 {
313        self.coordinator_lease_epoch
314    }
315
316    #[must_use]
317    pub fn coordinator_lease_id(&self) -> &str {
318        &self.coordinator_lease_id
319    }
320
321    #[must_use]
322    pub fn recovery_claimant_id(&self) -> &str {
323        &self.recovery_claimant_id
324    }
325
326    #[must_use]
327    pub const fn recovery_expires_at_unix_ms(&self) -> u64 {
328        self.recovery_expires_at_unix_ms
329    }
330
331    #[must_use]
332    pub const fn not_after_unix_ms(&self) -> Option<u64> {
333        self.not_after_unix_ms
334    }
335
336    #[must_use]
337    pub const fn store_fence(&self) -> &StoreMutationFence {
338        &self.store_fence
339    }
340}
341
342#[derive(Clone, Copy)]
343pub struct EconomicOperationStageContext<'a> {
344    operation: &'a AdmissionOperationV1,
345    recovery_lease: &'a AdmissionRecoveryLease,
346    not_after_unix_ms: Option<u64>,
347}
348
349pub(crate) struct EconomicStageAdmissionCheckpoint<'a> {
350    pub(crate) store_id: &'a str,
351    pub(crate) sequence: u64,
352    pub(crate) digest: &'a str,
353}
354
355struct EconomicStageOptions<'a> {
356    operation: Option<EconomicOperationStageContext<'a>>,
357    descriptor: Option<EconomicStateStageDescriptor>,
358    admission_checkpoint: Option<EconomicStageAdmissionCheckpoint<'a>>,
359    consumer_descriptor_kind: Option<&'static str>,
360}
361
362impl<'a> EconomicOperationStageContext<'a> {
363    #[must_use]
364    pub const fn new(
365        operation: &'a AdmissionOperationV1,
366        recovery_lease: &'a AdmissionRecoveryLease,
367    ) -> Self {
368        Self {
369            operation,
370            recovery_lease,
371            not_after_unix_ms: None,
372        }
373    }
374
375    pub fn with_not_after_unix_ms(
376        mut self,
377        not_after_unix_ms: u64,
378    ) -> Result<Self, EconomicStateCacheError> {
379        validate_trusted_time(not_after_unix_ms)?;
380        self.not_after_unix_ms = Some(not_after_unix_ms);
381        Ok(self)
382    }
383
384    #[must_use]
385    pub const fn operation(&self) -> &'a AdmissionOperationV1 {
386        self.operation
387    }
388
389    #[must_use]
390    pub const fn recovery_lease(&self) -> &'a AdmissionRecoveryLease {
391        self.recovery_lease
392    }
393}
394
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct EconomicStateStageRecord {
397    base_view: EconomicStateAnchorViewV1,
398    batch: EconomicStateBatchV1,
399    committed_view: Option<EconomicStateAnchorViewV1>,
400    operation_binding: Option<EconomicOperationStageBinding>,
401    descriptor: Option<EconomicStateStageDescriptor>,
402    status: EconomicStateStageStatus,
403    reason: Option<String>,
404    version: u64,
405    created_at_unix_ms: u64,
406    updated_at_unix_ms: u64,
407    snapshot_digest: String,
408}
409
410impl EconomicStateStageRecord {
411    #[must_use]
412    pub fn base_view(&self) -> &EconomicStateAnchorViewV1 {
413        &self.base_view
414    }
415
416    #[must_use]
417    pub fn batch(&self) -> &EconomicStateBatchV1 {
418        &self.batch
419    }
420
421    #[must_use]
422    pub fn committed_view(&self) -> Option<&EconomicStateAnchorViewV1> {
423        self.committed_view.as_ref()
424    }
425
426    #[must_use]
427    pub fn operation_binding(&self) -> Option<&EconomicOperationStageBinding> {
428        self.operation_binding.as_ref()
429    }
430
431    #[must_use]
432    pub fn not_after_unix_ms(&self) -> Option<u64> {
433        self.operation_binding
434            .as_ref()
435            .and_then(EconomicOperationStageBinding::not_after_unix_ms)
436    }
437
438    #[must_use]
439    pub fn descriptor(&self) -> Option<&EconomicStateStageDescriptor> {
440        self.descriptor.as_ref()
441    }
442
443    #[must_use]
444    pub fn status(&self) -> EconomicStateStageStatus {
445        self.status
446    }
447
448    #[must_use]
449    pub fn reason(&self) -> Option<&str> {
450        self.reason.as_deref()
451    }
452
453    #[must_use]
454    pub fn version(&self) -> u64 {
455        self.version
456    }
457}
458
459pub fn admission_terminal_projection_effect_result(
460    envelope: &SignedAdmissionTerminalProjectionV1,
461) -> Result<EconomicEffectTerminalV1, EconomicStateCacheError> {
462    let verified = envelope
463        .verify()
464        .map_err(|_| EconomicStateCacheError::Conflict)?;
465    let descriptor = admission_terminal_projection_descriptor(envelope, &verified)?;
466    terminal_projection_effect_result(&descriptor, &verified)
467}
468
469fn admission_terminal_projection_descriptor(
470    envelope: &SignedAdmissionTerminalProjectionV1,
471    verified: &VerifiedAdmissionTerminalProjectionV1,
472) -> Result<EconomicStateStageDescriptor, EconomicStateCacheError> {
473    EconomicStateStageDescriptor::new(
474        ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND,
475        verified
476            .source_operation()
477            .binding()
478            .operation_id()
479            .as_str(),
480        envelope,
481    )
482}
483
484fn terminal_projection_effect_result(
485    descriptor: &EconomicStateStageDescriptor,
486    verified: &VerifiedAdmissionTerminalProjectionV1,
487) -> Result<EconomicEffectTerminalV1, EconomicStateCacheError> {
488    let source = verified.source_operation();
489    let terminal = verified.terminal_operation();
490    let projection_digest = terminal
491        .terminal_replay()
492        .ok_or(EconomicStateCacheError::Conflict)?
493        .projection_digest()
494        .as_str()
495        .to_owned();
496    let source_operation_digest =
497        sha256_hex(&canonical_json_bytes(&source.to_persisted()).map_err(canonical_error)?);
498    let terminal_operation_digest =
499        sha256_hex(&canonical_json_bytes(&terminal.to_persisted()).map_err(canonical_error)?);
500    let commitment = AdmissionTerminalEffectCommitment {
501        schema: ADMISSION_TERMINAL_EFFECT_RESULT_SCHEMA,
502        descriptor_digest: descriptor.digest().to_owned(),
503        operation_id: source.binding().operation_id().as_str().to_owned(),
504        request_id: source.replay_key().request_id.as_str().to_owned(),
505        source_operation_digest,
506        terminal_operation_digest,
507        terminal_state: terminal.state(),
508        projection_digest: projection_digest.clone(),
509        records: verified
510            .records()
511            .iter()
512            .map(|record| AdmissionTerminalEffectRecordCommitment {
513                kind: record.kind(),
514                record_id: record.record_id().as_str().to_owned(),
515                record_digest: record.record_digest().as_str().to_owned(),
516            })
517            .collect(),
518    };
519    let result = EconomicContentV1::Inline {
520        value: serde_json::to_value(commitment).map_err(canonical_error)?,
521    };
522    Ok(EconomicEffectTerminalV1::Completed {
523        result_id: projection_digest,
524        result_digest: result.digest()?,
525        result,
526    })
527}
528
529fn qualify_generic_terminal_projection_effect_slot(
530    base_view: &EconomicStateAnchorViewV1,
531    batch: &EconomicStateBatchV1,
532    descriptor: &EconomicStateStageDescriptor,
533    verified: &VerifiedAdmissionTerminalProjectionV1,
534) -> Result<EconomicEffectSlotV1, EconomicStateCacheError> {
535    if batch.transitions.len() != 1
536        || !batch.effect_slots.is_empty()
537        || !batch.request_replays.is_empty()
538        || batch.transitions[0].prepared_effect.is_some()
539    {
540        return Err(EconomicStateCacheError::Conflict);
541    }
542    let transition = &batch.transitions[0];
543    if transition.resource_key.resource_family != "effect_slot" {
544        return Err(EconomicStateCacheError::Conflict);
545    }
546    let current_head = base_view
547        .head(&transition.resource_key)
548        .ok_or(EconomicStateCacheError::Conflict)?;
549    let current_slot = economic_effect_slot_from_head(current_head)
550        .map_err(|_| EconomicStateCacheError::Conflict)?;
551    let completed_slot = economic_effect_slot_from_head(&transition.next_head)
552        .map_err(|_| EconomicStateCacheError::Conflict)?;
553    current_slot
554        .validate_successor(&completed_slot)
555        .map_err(|_| EconomicStateCacheError::Conflict)?;
556    if completed_slot.state == EconomicEffectStateV1::NoEffect {
557        return verify_economic_cancellation_terminal_advance(base_view, batch, verified)
558            .map_err(|_| EconomicStateCacheError::Conflict);
559    }
560    let source = verified.source_operation();
561    let binding = source.binding();
562    let dispatch_commit = source
563        .dispatch_commit()
564        .ok_or(EconomicStateCacheError::Conflict)?;
565    let expected_terminal = terminal_projection_effect_result(descriptor, verified)?;
566    let expected_result = match &expected_terminal {
567        EconomicEffectTerminalV1::Completed {
568            result_id,
569            result_digest,
570            result,
571        } => EconomicTerminalResultV1 {
572            result_id: result_id.clone(),
573            result_digest: result_digest.clone(),
574            result: result.clone(),
575        },
576        EconomicEffectTerminalV1::NoEffect { .. } => return Err(EconomicStateCacheError::Conflict),
577    };
578    if current_slot.state != EconomicEffectStateV1::DispatchCommitted
579        && current_slot.state != EconomicEffectStateV1::Unknown
580        || completed_slot.state != EconomicEffectStateV1::Completed
581        || completed_slot.terminal.as_ref() != Some(&expected_terminal)
582        || completed_slot.operation_id != binding.operation_id().as_str()
583        || completed_slot.request.request_namespace_digest
584            != binding.request_namespace_digest().as_str()
585        || completed_slot.request.request_id != binding.request_id().as_str()
586        || completed_slot.request.request_binding_digest != binding.request_binding_hash().as_str()
587        || completed_slot.parameters_digest != binding.action_parameter_hash().as_str()
588        || completed_slot.admission_handoff.state
589            != EconomicAdmissionHandoffStateV1::DispatchCommitted
590        || completed_slot.admission_handoff.operation_version != dispatch_commit.committed_version
591        || completed_slot.admission_handoff.lifecycle_fence
592            != dispatch_commit.coordinator_lease_epoch
593        || completed_slot.admission_handoff.store_fence != dispatch_commit.store_fence
594        || !current_fence_serves_historical(
595            &dispatch_commit.store_fence,
596            &verified.context().store_fence,
597        )
598        || batch.operation_id.as_deref() != Some(binding.operation_id().as_str())
599        || transition.next_head.lifecycle_state != "completed"
600        || transition.next_head.terminal_result.as_ref() != Some(&expected_result)
601    {
602        return Err(EconomicStateCacheError::Conflict);
603    }
604    Ok(completed_slot)
605}
606
607fn qualify_terminal_projection_advance(
608    advance: &VerifiedEconomicStateBatchAdvance,
609    descriptor: &EconomicStateStageDescriptor,
610    verified: &VerifiedAdmissionTerminalProjectionV1,
611) -> Result<EconomicEffectSlotV1, EconomicStateCacheError> {
612    match verified.channel_terminal() {
613        Some(channel) => channel
614            .qualify_anchored_advance(advance)
615            .cloned()
616            .map_err(|_| EconomicStateCacheError::Conflict),
617        None => qualify_generic_terminal_projection_effect_slot(
618            advance.current().view(),
619            advance.batch(),
620            descriptor,
621            verified,
622        ),
623    }
624}
625
626fn qualify_retained_terminal_projection_advance(
627    base_view: &EconomicStateAnchorViewV1,
628    batch: &EconomicStateBatchV1,
629    descriptor: &EconomicStateStageDescriptor,
630    verified: &VerifiedAdmissionTerminalProjectionV1,
631) -> Result<EconomicEffectSlotV1, EconomicStateCacheError> {
632    match verified.channel_terminal() {
633        Some(channel) => channel
634            .qualify_retained_anchored_advance(base_view, batch)
635            .cloned()
636            .map_err(|_| EconomicStateCacheError::Conflict),
637        None => {
638            qualify_generic_terminal_projection_effect_slot(base_view, batch, descriptor, verified)
639        }
640    }
641}
642
643fn committed_view_matches_batch(
644    committed: &EconomicStateAnchorViewV1,
645    batch: &EconomicStateBatchV1,
646) -> bool {
647    batch
648        .transitions
649        .iter()
650        .all(|transition| committed.head(&transition.resource_key) == Some(&transition.next_head))
651}
652
653fn batch_contains_channel_content(batch: &EconomicStateBatchV1) -> bool {
654    batch.transitions.iter().any(|transition| {
655        matches!(
656            transition.resource_key.resource_family.as_str(),
657            CHANNEL_LIFECYCLE_RESOURCE_FAMILY | CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY
658        )
659    }) || batch
660        .effect_slots
661        .iter()
662        .any(|effect| effect.effect_kind == CHANNEL_SERVICE_DISPATCH_EFFECT_KIND)
663}
664
665fn is_protected_channel_stage(record: &EconomicStateStageRecord) -> bool {
666    record
667        .descriptor
668        .as_ref()
669        .is_some_and(|descriptor| descriptor.kind == CHANNEL_TRANSITION_REPLAY_FORMAT)
670        || batch_contains_channel_content(&record.batch)
671}
672
673fn validate_stage_options(
674    batch: &EconomicStateBatchV1,
675    options: &EconomicStageOptions<'_>,
676) -> Result<(), EconomicStateCacheError> {
677    if let Some(descriptor) = &options.descriptor {
678        descriptor.validate()?;
679        let reserved = matches!(
680            descriptor.kind(),
681            CLEARING_LIFECYCLE_REPLAY_DESCRIPTOR_KIND
682                | ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND
683                | CHANNEL_TRANSITION_REPLAY_FORMAT
684        );
685        if reserved != (options.consumer_descriptor_kind == Some(descriptor.kind()))
686            || options
687                .consumer_descriptor_kind
688                .is_some_and(|kind| kind != descriptor.kind())
689        {
690            return Err(EconomicStateCacheError::Conflict);
691        }
692    } else if options.consumer_descriptor_kind.is_some() {
693        return Err(EconomicStateCacheError::Conflict);
694    }
695    let channel_content = batch_contains_channel_content(batch);
696    let channel_consumer = matches!(
697        options.consumer_descriptor_kind,
698        Some(CHANNEL_TRANSITION_REPLAY_FORMAT | ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND)
699    );
700    let reservation_consumer =
701        options.consumer_descriptor_kind == Some(CHANNEL_TRANSITION_REPLAY_FORMAT);
702    if channel_content && !channel_consumer || !channel_content && reservation_consumer {
703        return Err(EconomicStateCacheError::Conflict);
704    }
705    Ok(())
706}
707
708#[allow(clippy::too_many_arguments)]
709pub(crate) fn stage_channel_batch_in_transaction(
710    transaction: &Transaction<'_>,
711    advance: &VerifiedEconomicStateBatchAdvance,
712    operation: EconomicOperationStageContext<'_>,
713    descriptor: EconomicStateStageDescriptor,
714    active_fence: &StoreMutationFence,
715    trusted_now_unix_ms: u64,
716    serving_owner: &SqliteServingOwner,
717) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
718    validate_trusted_time(trusted_now_unix_ms)?;
719    let options = EconomicStageOptions {
720        operation: Some(operation),
721        descriptor: Some(descriptor),
722        admission_checkpoint: None,
723        consumer_descriptor_kind: Some(CHANNEL_TRANSITION_REPLAY_FORMAT),
724    };
725    validate_stage_options(advance.batch(), &options)?;
726    stage_batch_in_transaction(
727        transaction,
728        advance,
729        options,
730        active_fence,
731        trusted_now_unix_ms,
732        serving_owner,
733    )
734}
735
736pub(crate) fn record_channel_anchor_advanced_in_transaction(
737    transaction: &Transaction<'_>,
738    advance: &VerifiedEconomicStateBatchAdvance,
739    committed: &VerifiedEconomicStateView,
740    trusted_now_unix_ms: u64,
741    serving_owner: &SqliteServingOwner,
742) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
743    validate_trusted_time(trusted_now_unix_ms)?;
744    if !committed_view_matches_batch(committed.view(), advance.batch()) {
745        return Err(EconomicStateCacheError::Conflict);
746    }
747    let committed_bytes = canonical_bounded(committed.view(), MAX_VIEW_BYTES, "committed view")?;
748    let mut record = load_stage_tx(transaction, &advance.batch().batch_id)?
749        .ok_or(EconomicStateCacheError::NotFound)?;
750    if record.base_view != *advance.current().view()
751        || record.batch != *advance.batch()
752        || record
753            .descriptor
754            .as_ref()
755            .is_none_or(|descriptor| descriptor.kind != CHANNEL_TRANSITION_REPLAY_FORMAT)
756        || !is_protected_channel_stage(&record)
757    {
758        return Err(EconomicStateCacheError::Conflict);
759    }
760    if matches!(
761        record.status,
762        EconomicStateStageStatus::EconomicAnchorAdvanced | EconomicStateStageStatus::DbFinalized
763    ) {
764        return if record.committed_view.as_ref() == Some(committed.view()) {
765            Ok(record)
766        } else {
767            Err(EconomicStateCacheError::Conflict)
768        };
769    }
770    require_transition(
771        record.status,
772        EconomicStateStageStatus::EconomicAnchorAdvanced,
773    )?;
774    record.status = EconomicStateStageStatus::EconomicAnchorAdvanced;
775    record.committed_view = Some(committed.view().clone());
776    record.version = next_version(record.version)?;
777    record.updated_at_unix_ms = monotonic_time(&record, trusted_now_unix_ms)?;
778    record.snapshot_digest = stage_snapshot_digest(&record, &[])?;
779    update_stage(
780        transaction,
781        &record,
782        Some(committed_bytes),
783        None,
784        EconomicStateStageStatus::DbStaged,
785    )?;
786    append_stage_commit(
787        transaction,
788        &record,
789        "channel_anchor_advanced",
790        serving_owner,
791    )?;
792    Ok(record)
793}
794
795fn stage_batch_in_transaction(
796    transaction: &Transaction<'_>,
797    advance: &VerifiedEconomicStateBatchAdvance,
798    options: EconomicStageOptions<'_>,
799    active_fence: &StoreMutationFence,
800    trusted_now_unix_ms: u64,
801    serving_owner: &SqliteServingOwner,
802) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
803    let EconomicStageOptions {
804        operation,
805        descriptor,
806        admission_checkpoint,
807        consumer_descriptor_kind: _,
808    } = options;
809    if let Some(existing) = load_stage_tx(transaction, &advance.batch().batch_id)? {
810        if existing.base_view != *advance.current().view() || existing.batch != *advance.batch() {
811            return Err(EconomicStateCacheError::Conflict);
812        }
813        let qualified = qualify_operation_binding(
814            transaction,
815            advance.batch(),
816            operation,
817            active_fence,
818            trusted_now_unix_ms,
819        )?;
820        if qualified != existing.operation_binding || descriptor != existing.descriptor {
821            return Err(EconomicStateCacheError::Conflict);
822        }
823        return Ok(existing);
824    }
825    if let Some(descriptor) = &descriptor {
826        let retained_batch_id = transaction
827            .query_row(
828                r#"
829                SELECT batch_id FROM economic_state_stages
830                WHERE descriptor_kind = ?1 AND descriptor_key = ?2
831                "#,
832                params![descriptor.kind(), descriptor.key()],
833                |row| row.get::<_, String>(0),
834            )
835            .optional()
836            .map_err(sqlite_error)?;
837        if retained_batch_id.is_some() {
838            return Err(EconomicStateCacheError::Conflict);
839        }
840    }
841    if let Some(checkpoint) = admission_checkpoint {
842        verify_stage_admission_checkpoint(
843            transaction,
844            &checkpoint,
845            active_fence,
846            trusted_now_unix_ms,
847        )?;
848    }
849    let operation_binding = qualify_operation_binding(
850        transaction,
851        advance.batch(),
852        operation,
853        active_fence,
854        trusted_now_unix_ms,
855    )?;
856    let operation_binding_bytes = operation_binding
857        .as_ref()
858        .map(canonical_json_bytes)
859        .transpose()
860        .map_err(canonical_error)?;
861    let mut record = EconomicStateStageRecord {
862        base_view: advance.current().view().clone(),
863        batch: advance.batch().clone(),
864        committed_view: None,
865        operation_binding,
866        descriptor,
867        status: EconomicStateStageStatus::DbStaged,
868        reason: None,
869        version: 1,
870        created_at_unix_ms: trusted_now_unix_ms,
871        updated_at_unix_ms: trusted_now_unix_ms,
872        snapshot_digest: String::new(),
873    };
874    record.snapshot_digest = stage_snapshot_digest(&record, &[])?;
875    let base_view_bytes = canonical_bounded(advance.current().view(), MAX_VIEW_BYTES, "base view")?;
876    let batch_bytes = advance.batch().canonical_bytes()?;
877    transaction
878        .execute(
879            r#"
880            INSERT INTO economic_state_stages (
881                batch_id, checkpoint_sequence, checkpoint_digest,
882                base_view_json, batch_json, committed_view_json,
883                operation_binding_json, descriptor_kind, descriptor_key,
884                descriptor_digest, descriptor_json, status, reason,
885                stage_version, snapshot_digest, created_at_unix_ms,
886                updated_at_unix_ms
887            ) VALUES (
888                ?1, ?2, ?3, ?4, ?5, NULL, ?6, ?7, ?8, ?9, ?10,
889                ?11, NULL, 1, ?12, ?13, ?13
890            )
891            "#,
892            params![
893                &record.batch.batch_id,
894                sqlite_i64(record.batch.checkpoint_sequence, "checkpoint_sequence")?,
895                &record.batch.checkpoint_digest,
896                base_view_bytes,
897                batch_bytes,
898                operation_binding_bytes,
899                record.descriptor.as_ref().map(|value| value.kind.as_str()),
900                record.descriptor.as_ref().map(|value| value.key.as_str()),
901                record
902                    .descriptor
903                    .as_ref()
904                    .map(|value| value.digest.as_str()),
905                record
906                    .descriptor
907                    .as_ref()
908                    .map(|value| value.canonical_json.as_slice()),
909                record.status.as_str(),
910                &record.snapshot_digest,
911                sqlite_i64(trusted_now_unix_ms, "trusted_now_unix_ms")?,
912            ],
913        )
914        .map_err(sqlite_error)?;
915    append_stage_commit(transaction, &record, "stage_batch", serving_owner)?;
916    Ok(record)
917}
918
919#[derive(Clone)]
920pub struct SqliteEconomicStateCache {
921    connection: Arc<Mutex<Connection>>,
922    serving_owner: Arc<SqliteServingOwner>,
923}
924
925impl SqliteEconomicStateCache {
926    pub(crate) fn open_alongside(
927        connection: Arc<Mutex<Connection>>,
928        serving_owner: Arc<SqliteServingOwner>,
929    ) -> Self {
930        Self {
931            connection,
932            serving_owner,
933        }
934    }
935
936    fn connection(&self) -> Result<MutexGuard<'_, Connection>, EconomicStateCacheError> {
937        self.connection
938            .lock()
939            .map_err(|_| invariant("economic state cache lock is poisoned"))
940    }
941
942    fn begin_read<'a>(
943        &self,
944        connection: &'a mut Connection,
945    ) -> Result<Transaction<'a>, EconomicStateCacheError> {
946        let transaction = connection
947            .transaction_with_behavior(TransactionBehavior::Deferred)
948            .map_err(sqlite_error)?;
949        verify_active_owner(&transaction, &self.serving_owner, None)?;
950        self.serving_owner
951            .verify_authority_anchor(&transaction)
952            .map_err(map_owner_error)?;
953        verify_cache_sql_invariants(&transaction)?;
954        Ok(transaction)
955    }
956
957    fn begin_write<'a>(
958        &self,
959        connection: &'a mut Connection,
960        fence: &StoreMutationFence,
961    ) -> Result<Transaction<'a>, EconomicStateCacheError> {
962        let transaction = connection
963            .transaction_with_behavior(TransactionBehavior::Immediate)
964            .map_err(sqlite_error)?;
965        verify_active_owner(&transaction, &self.serving_owner, Some(fence))?;
966        self.serving_owner
967            .verify_authority_anchor(&transaction)
968            .map_err(map_owner_error)?;
969        verify_cache_sql_invariants(&transaction)?;
970        Ok(transaction)
971    }
972
973    fn commit_write(&self, transaction: Transaction<'_>) -> Result<(), EconomicStateCacheError> {
974        transaction.commit().map_err(|error| {
975            map_owner_error(self.serving_owner.outcome_unknown(format!(
976                "sqlite economic state cache commit outcome is unknown: {error}"
977            )))
978        })
979    }
980
981    fn sync_after_write(&self, connection: &Connection) -> Result<(), EconomicStateCacheError> {
982        self.serving_owner
983            .sync_authority_anchor(connection)
984            .map_err(map_owner_error)
985    }
986
987    pub fn stage_batch(
988        &self,
989        advance: &VerifiedEconomicStateBatchAdvance,
990        operation: Option<EconomicOperationStageContext<'_>>,
991        active_fence: &StoreMutationFence,
992        trusted_now_unix_ms: u64,
993    ) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
994        self.stage_batch_with_descriptor(
995            advance,
996            operation,
997            None,
998            active_fence,
999            trusted_now_unix_ms,
1000        )
1001    }
1002
1003    pub fn stage_batch_with_descriptor(
1004        &self,
1005        advance: &VerifiedEconomicStateBatchAdvance,
1006        operation: Option<EconomicOperationStageContext<'_>>,
1007        descriptor: Option<EconomicStateStageDescriptor>,
1008        active_fence: &StoreMutationFence,
1009        trusted_now_unix_ms: u64,
1010    ) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
1011        self.stage_batch_inner(
1012            advance,
1013            EconomicStageOptions {
1014                operation,
1015                descriptor,
1016                admission_checkpoint: None,
1017                consumer_descriptor_kind: None,
1018            },
1019            active_fence,
1020            trusted_now_unix_ms,
1021        )
1022    }
1023
1024    pub fn stage_admission_terminal_projection(
1025        &self,
1026        advance: &VerifiedEconomicStateBatchAdvance,
1027        operation: EconomicOperationStageContext<'_>,
1028        envelope: &SignedAdmissionTerminalProjectionV1,
1029        active_fence: &StoreMutationFence,
1030        trusted_now_unix_ms: u64,
1031    ) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
1032        validate_trusted_time(trusted_now_unix_ms)?;
1033        let verified = envelope
1034            .verify()
1035            .map_err(|_| EconomicStateCacheError::Conflict)?;
1036        let descriptor = admission_terminal_projection_descriptor(envelope, &verified)?;
1037        let context = verified.context();
1038        let source = verified.source_operation();
1039        let lease = operation.recovery_lease;
1040        let expected_claimant = format!("kernel:{}", verified.signer_key().to_hex());
1041        if operation.operation != source
1042            || context.operation_id != *source.binding().operation_id()
1043            || context.request_id != source.replay_key().request_id
1044            || context.expected_operation_version != source.version()
1045            || context.coordinator_lease_id != *lease.coordinator_lease_id()
1046            || context.coordinator_lease_epoch != lease.coordinator_lease_epoch()
1047            || context.store_fence != *lease.store_fence()
1048            || lease.claimant_id().as_str() != expected_claimant
1049            || context.trusted_time_unix_ms > trusted_now_unix_ms
1050            || context.trusted_time_unix_ms >= lease.expires_at_unix_ms()
1051        {
1052            return Err(EconomicStateCacheError::Conflict);
1053        }
1054        qualify_terminal_projection_advance(advance, &descriptor, &verified)?;
1055        self.stage_batch_inner(
1056            advance,
1057            EconomicStageOptions {
1058                operation: Some(operation),
1059                descriptor: Some(descriptor),
1060                admission_checkpoint: None,
1061                consumer_descriptor_kind: Some(ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND),
1062            },
1063            active_fence,
1064            trusted_now_unix_ms,
1065        )
1066    }
1067
1068    pub(crate) fn stage_clearing_lifecycle_batch(
1069        &self,
1070        advance: &VerifiedEconomicStateBatchAdvance,
1071        descriptor: EconomicStateStageDescriptor,
1072        checkpoint: Option<EconomicStageAdmissionCheckpoint<'_>>,
1073        active_fence: &StoreMutationFence,
1074        trusted_now_unix_ms: u64,
1075    ) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
1076        self.stage_batch_inner(
1077            advance,
1078            EconomicStageOptions {
1079                operation: None,
1080                descriptor: Some(descriptor),
1081                admission_checkpoint: checkpoint,
1082                consumer_descriptor_kind: Some(CLEARING_LIFECYCLE_REPLAY_DESCRIPTOR_KIND),
1083            },
1084            active_fence,
1085            trusted_now_unix_ms,
1086        )
1087    }
1088
1089    fn stage_batch_inner(
1090        &self,
1091        advance: &VerifiedEconomicStateBatchAdvance,
1092        options: EconomicStageOptions<'_>,
1093        active_fence: &StoreMutationFence,
1094        trusted_now_unix_ms: u64,
1095    ) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
1096        validate_trusted_time(trusted_now_unix_ms)?;
1097        validate_stage_options(advance.batch(), &options)?;
1098        let mut connection = self.connection()?;
1099        let transaction = self.begin_write(&mut connection, active_fence)?;
1100        let record = stage_batch_in_transaction(
1101            &transaction,
1102            advance,
1103            options,
1104            active_fence,
1105            trusted_now_unix_ms,
1106            &self.serving_owner,
1107        )?;
1108        self.commit_write(transaction)?;
1109        self.sync_after_write(&connection)?;
1110        Ok(record)
1111    }
1112
1113    pub fn record_anchor_advanced(
1114        &self,
1115        advance: &VerifiedEconomicStateBatchAdvance,
1116        committed: &VerifiedEconomicStateView,
1117        pins: &EconomicStateAnchorPins,
1118        active_fence: &StoreMutationFence,
1119        trusted_now_unix_ms: u64,
1120    ) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
1121        validate_trusted_time(trusted_now_unix_ms)?;
1122        verify_economic_state_batch_commit(advance, committed, pins)?;
1123        if let Some(descriptor) = self
1124            .load_stage(&advance.batch().batch_id)?
1125            .and_then(|stage| stage.descriptor().cloned())
1126            .filter(|descriptor| descriptor.kind() == ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND)
1127        {
1128            let envelope = descriptor.decode::<SignedAdmissionTerminalProjectionV1>()?;
1129            let verified = envelope
1130                .verify()
1131                .map_err(|_| EconomicStateCacheError::Conflict)?;
1132            let terminal_slot =
1133                qualify_terminal_projection_advance(advance, &descriptor, &verified)?;
1134            if !committed_view_matches_batch(committed.view(), advance.batch()) {
1135                return Err(EconomicStateCacheError::Conflict);
1136            }
1137            if terminal_slot.state == EconomicEffectStateV1::Completed {
1138                verify_economic_completed_effect(committed, &terminal_slot)?;
1139            }
1140        }
1141        if self
1142            .load_stage(&advance.batch().batch_id)?
1143            .is_some_and(|stage| {
1144                stage
1145                    .descriptor()
1146                    .is_some_and(|descriptor| descriptor.kind() == CHANNEL_TRANSITION_REPLAY_FORMAT)
1147                    || (batch_contains_channel_content(stage.batch())
1148                        && stage.descriptor().is_none_or(|descriptor| {
1149                            descriptor.kind() != ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND
1150                        }))
1151            })
1152        {
1153            return Err(EconomicStateCacheError::Conflict);
1154        }
1155        let committed_bytes =
1156            canonical_bounded(committed.view(), MAX_VIEW_BYTES, "committed view")?;
1157        let mut connection = self.connection()?;
1158        let transaction = self.begin_write(&mut connection, active_fence)?;
1159        let mut record = load_stage_tx(&transaction, &advance.batch().batch_id)?
1160            .ok_or(EconomicStateCacheError::NotFound)?;
1161        if record.base_view != *advance.current().view() || record.batch != *advance.batch() {
1162            return Err(EconomicStateCacheError::Conflict);
1163        }
1164        if matches!(
1165            record.status,
1166            EconomicStateStageStatus::EconomicAnchorAdvanced
1167                | EconomicStateStageStatus::DbFinalized
1168        ) {
1169            if record.committed_view.as_ref() == Some(committed.view()) {
1170                transaction.commit().map_err(sqlite_error)?;
1171                return Ok(record);
1172            }
1173            return Err(EconomicStateCacheError::Conflict);
1174        }
1175        require_transition(
1176            record.status,
1177            EconomicStateStageStatus::EconomicAnchorAdvanced,
1178        )?;
1179        record.status = EconomicStateStageStatus::EconomicAnchorAdvanced;
1180        record.committed_view = Some(committed.view().clone());
1181        record.version = next_version(record.version)?;
1182        record.updated_at_unix_ms = monotonic_time(&record, trusted_now_unix_ms)?;
1183        record.snapshot_digest = stage_snapshot_digest(&record, &[])?;
1184        update_stage(
1185            &transaction,
1186            &record,
1187            Some(committed_bytes),
1188            None,
1189            EconomicStateStageStatus::DbStaged,
1190        )?;
1191        append_stage_commit(
1192            &transaction,
1193            &record,
1194            "anchor_advanced",
1195            &self.serving_owner,
1196        )?;
1197        self.commit_write(transaction)?;
1198        self.sync_after_write(&connection)?;
1199        Ok(record)
1200    }
1201
1202    pub fn finalize_stage(
1203        &self,
1204        batch_id: &str,
1205        active_fence: &StoreMutationFence,
1206        trusted_now_unix_ms: u64,
1207    ) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
1208        validate_digest(batch_id, "batch_id")?;
1209        validate_trusted_time(trusted_now_unix_ms)?;
1210        let mut connection = self.connection()?;
1211        let transaction = self.begin_write(&mut connection, active_fence)?;
1212        if load_stage_tx(&transaction, batch_id)?.is_some_and(|record| {
1213            is_protected_channel_stage(&record)
1214                || record.descriptor.as_ref().is_some_and(|descriptor| {
1215                    descriptor.kind == ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND
1216                })
1217        }) {
1218            return Err(EconomicStateCacheError::Conflict);
1219        }
1220        let record = finalize_stage_in_transaction(
1221            &transaction,
1222            batch_id,
1223            &self.serving_owner,
1224            trusted_now_unix_ms,
1225        )?;
1226        self.commit_write(transaction)?;
1227        self.sync_after_write(&connection)?;
1228        Ok(record)
1229    }
1230
1231    pub fn discard_unanchored_stage(
1232        &self,
1233        batch_id: &str,
1234        reason: &str,
1235        active_fence: &StoreMutationFence,
1236        trusted_now_unix_ms: u64,
1237    ) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
1238        self.close_stage(
1239            batch_id,
1240            reason,
1241            EconomicStateStageStatus::Discarded,
1242            active_fence,
1243            trusted_now_unix_ms,
1244        )
1245    }
1246
1247    pub fn quarantine_stage(
1248        &self,
1249        batch_id: &str,
1250        reason: &str,
1251        active_fence: &StoreMutationFence,
1252        trusted_now_unix_ms: u64,
1253    ) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
1254        self.close_stage(
1255            batch_id,
1256            reason,
1257            EconomicStateStageStatus::Quarantined,
1258            active_fence,
1259            trusted_now_unix_ms,
1260        )
1261    }
1262
1263    fn close_stage(
1264        &self,
1265        batch_id: &str,
1266        reason: &str,
1267        target: EconomicStateStageStatus,
1268        active_fence: &StoreMutationFence,
1269        trusted_now_unix_ms: u64,
1270    ) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
1271        validate_digest(batch_id, "batch_id")?;
1272        validate_reason(reason)?;
1273        validate_trusted_time(trusted_now_unix_ms)?;
1274        let mut connection = self.connection()?;
1275        let transaction = self.begin_write(&mut connection, active_fence)?;
1276        let mut record =
1277            load_stage_tx(&transaction, batch_id)?.ok_or(EconomicStateCacheError::NotFound)?;
1278        if is_protected_channel_stage(&record)
1279            || record.descriptor.as_ref().is_some_and(|descriptor| {
1280                descriptor.kind == ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND
1281            })
1282        {
1283            return Err(EconomicStateCacheError::Conflict);
1284        }
1285        if record.status == target && record.reason.as_deref() == Some(reason) {
1286            transaction.commit().map_err(sqlite_error)?;
1287            return Ok(record);
1288        }
1289        let previous_status = record.status;
1290        require_transition(previous_status, target)?;
1291        record.status = target;
1292        record.reason = Some(reason.to_owned());
1293        record.version = next_version(record.version)?;
1294        record.updated_at_unix_ms = monotonic_time(&record, trusted_now_unix_ms)?;
1295        record.snapshot_digest = stage_snapshot_digest(&record, &[])?;
1296        update_stage(&transaction, &record, None, Some(reason), previous_status)?;
1297        append_stage_commit(
1298            &transaction,
1299            &record,
1300            if target == EconomicStateStageStatus::Discarded {
1301                "discard_stage"
1302            } else {
1303                "quarantine_stage"
1304            },
1305            &self.serving_owner,
1306        )?;
1307        self.commit_write(transaction)?;
1308        self.sync_after_write(&connection)?;
1309        Ok(record)
1310    }
1311
1312    pub fn load_stage(
1313        &self,
1314        batch_id: &str,
1315    ) -> Result<Option<EconomicStateStageRecord>, EconomicStateCacheError> {
1316        validate_digest(batch_id, "batch_id")?;
1317        let mut connection = self.connection()?;
1318        let transaction = self.begin_read(&mut connection)?;
1319        let record = load_stage_tx(&transaction, batch_id)?;
1320        transaction.commit().map_err(sqlite_error)?;
1321        Ok(record)
1322    }
1323
1324    pub(crate) fn load_stage_by_descriptor(
1325        &self,
1326        kind: &str,
1327        key: &str,
1328    ) -> Result<Option<EconomicStateStageRecord>, EconomicStateCacheError> {
1329        validate_descriptor_text(kind, MAX_DESCRIPTOR_KIND_BYTES, "stage descriptor kind")?;
1330        validate_descriptor_text(key, MAX_DESCRIPTOR_KEY_BYTES, "stage descriptor key")?;
1331        let mut connection = self.connection()?;
1332        let transaction = self.begin_read(&mut connection)?;
1333        let batch_id = transaction
1334            .query_row(
1335                r#"
1336                SELECT batch_id FROM economic_state_stages
1337                WHERE descriptor_kind = ?1 AND descriptor_key = ?2
1338                "#,
1339                params![kind, key],
1340                |row| row.get::<_, String>(0),
1341            )
1342            .optional()
1343            .map_err(sqlite_error)?;
1344        let record = batch_id
1345            .as_deref()
1346            .map(|batch_id| load_stage_tx(&transaction, batch_id))
1347            .transpose()?
1348            .flatten();
1349        transaction.commit().map_err(sqlite_error)?;
1350        Ok(record)
1351    }
1352
1353    pub fn list_pending(
1354        &self,
1355        limit: usize,
1356    ) -> Result<Vec<EconomicStateStageRecord>, EconomicStateCacheError> {
1357        if limit == 0 || limit > 256 {
1358            return Err(invariant("economic recovery limit must be within 1..=256"));
1359        }
1360        let mut connection = self.connection()?;
1361        let transaction = self.begin_read(&mut connection)?;
1362        let mut statement = transaction
1363            .prepare(
1364                r#"
1365                SELECT batch_id FROM economic_state_stages
1366                WHERE status IN ('db_staged', 'economic_anchor_advanced')
1367                ORDER BY checkpoint_sequence, batch_id LIMIT ?1
1368                "#,
1369            )
1370            .map_err(sqlite_error)?;
1371        let batch_ids = statement
1372            .query_map(
1373                [i64::try_from(limit).map_err(|_| invariant("limit overflow"))?],
1374                |row| row.get::<_, String>(0),
1375            )
1376            .map_err(sqlite_error)?
1377            .collect::<Result<Vec<_>, _>>()
1378            .map_err(sqlite_error)?;
1379        drop(statement);
1380        let records = batch_ids
1381            .iter()
1382            .map(|batch_id| {
1383                load_stage_tx(&transaction, batch_id)?
1384                    .ok_or_else(|| invariant("pending economic stage disappeared"))
1385            })
1386            .collect::<Result<Vec<_>, _>>()?;
1387        transaction.commit().map_err(sqlite_error)?;
1388        Ok(records)
1389    }
1390
1391    pub fn load_finalized_head(
1392        &self,
1393        key: &EconomicResourceKeyV1,
1394    ) -> Result<Option<EconomicResourceHeadV1>, EconomicStateCacheError> {
1395        key.validate()?;
1396        let key_bytes = canonical_json_bytes(key).map_err(canonical_error)?;
1397        let key_digest = sha256_hex(&key_bytes);
1398        let mut connection = self.connection()?;
1399        let transaction = self.begin_read(&mut connection)?;
1400        let stored = transaction
1401            .query_row(
1402                r#"
1403                SELECT current.resource_key_json, current.head_digest,
1404                       current.head_json, current.source_batch_id,
1405                       staged.head_digest, staged.head_json
1406                FROM economic_state_heads AS current
1407                JOIN economic_state_stage_heads AS staged
1408                  ON staged.batch_id = current.source_batch_id
1409                 AND staged.resource_key_digest = current.resource_key_digest
1410                JOIN economic_state_stages AS stage
1411                  ON stage.batch_id = current.source_batch_id
1412                 AND stage.status = 'db_finalized'
1413                WHERE current.resource_key_digest = ?1
1414                "#,
1415                [&key_digest],
1416                |row| {
1417                    Ok((
1418                        row.get::<_, Vec<u8>>(0)?,
1419                        row.get::<_, String>(1)?,
1420                        row.get::<_, Vec<u8>>(2)?,
1421                        row.get::<_, String>(3)?,
1422                        row.get::<_, String>(4)?,
1423                        row.get::<_, Vec<u8>>(5)?,
1424                    ))
1425                },
1426            )
1427            .optional()
1428            .map_err(sqlite_error)?;
1429        let result = stored
1430            .map(
1431                |(stored_key, current_digest, current_head, _batch, staged_digest, staged_head)| {
1432                    if stored_key != key_bytes
1433                        || current_digest != staged_digest
1434                        || current_head != staged_head
1435                    {
1436                        return Err(invariant(
1437                            "current economic head lost its finalized projection",
1438                        ));
1439                    }
1440                    let head: EconomicResourceHeadV1 =
1441                        decode_exact(&current_head, "resource head")?;
1442                    if head.resource_key != *key || head.digest()? != current_digest {
1443                        return Err(invariant("cached economic resource head is corrupt"));
1444                    }
1445                    Ok(head)
1446                },
1447            )
1448            .transpose()?;
1449        transaction.commit().map_err(sqlite_error)?;
1450        Ok(result)
1451    }
1452}
1453
1454pub(crate) fn load_anchored_terminal_projection_in_transaction(
1455    transaction: &Transaction<'_>,
1456    batch_id: &str,
1457    active_fence: &StoreMutationFence,
1458    require_anchored: bool,
1459) -> Result<
1460    (
1461        VerifiedAdmissionTerminalProjectionV1,
1462        EconomicOperationStageBinding,
1463    ),
1464    EconomicStateCacheError,
1465> {
1466    validate_digest(batch_id, "batch_id")?;
1467    verify_cache_sql_invariants(transaction)?;
1468    let record = load_stage_tx(transaction, batch_id)?.ok_or(EconomicStateCacheError::NotFound)?;
1469    if matches!(
1470        record.status,
1471        EconomicStateStageStatus::Discarded | EconomicStateStageStatus::Quarantined
1472    ) || require_anchored
1473        && !matches!(
1474            record.status,
1475            EconomicStateStageStatus::EconomicAnchorAdvanced
1476                | EconomicStateStageStatus::DbFinalized
1477        )
1478    {
1479        return Err(EconomicStateCacheError::Conflict);
1480    }
1481    let descriptor = record
1482        .descriptor
1483        .as_ref()
1484        .ok_or(EconomicStateCacheError::Conflict)?;
1485    let envelope = descriptor.decode::<SignedAdmissionTerminalProjectionV1>()?;
1486    let verified = envelope
1487        .verify()
1488        .map_err(|_| EconomicStateCacheError::Conflict)?;
1489    let context = verified.context();
1490    let source = verified.source_operation();
1491    let operation_id = source.binding().operation_id().as_str();
1492    let binding = record
1493        .operation_binding
1494        .as_ref()
1495        .ok_or(EconomicStateCacheError::Conflict)?;
1496    qualify_retained_terminal_projection_advance(
1497        &record.base_view,
1498        &record.batch,
1499        descriptor,
1500        &verified,
1501    )?;
1502    let committed_head_matches = match record.committed_view.as_ref() {
1503        Some(committed) => committed_view_matches_batch(committed, &record.batch),
1504        None => record.status == EconomicStateStageStatus::DbStaged,
1505    };
1506    let expected_claimant = format!("kernel:{}", verified.signer_key().to_hex());
1507    if descriptor.kind != ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND
1508        || descriptor.key != operation_id
1509        || record.batch.operation_id.as_deref() != Some(operation_id)
1510        || binding.operation_id != operation_id
1511        || binding.operation_state != source.state()
1512        || binding.operation_version != source.version()
1513        || binding.coordinator_lease_epoch != source.coordinator_lease_epoch()
1514        || binding.coordinator_lease_id != context.coordinator_lease_id.as_str()
1515        || binding.recovery_claimant_id != expected_claimant
1516        || binding.recovery_expires_at_unix_ms <= context.trusted_time_unix_ms
1517        || !current_fence_serves_historical(&binding.store_fence, active_fence)
1518        || context.store_fence != binding.store_fence
1519        || context.trusted_time_unix_ms > record.created_at_unix_ms
1520        || !committed_head_matches
1521    {
1522        return Err(EconomicStateCacheError::Conflict);
1523    }
1524    Ok((verified, binding.clone()))
1525}
1526
1527pub(crate) fn has_reserved_terminal_stage(
1528    transaction: &Transaction<'_>,
1529    operation_id: &str,
1530) -> Result<bool, EconomicStateCacheError> {
1531    transaction
1532        .query_row(
1533            r#"
1534            SELECT EXISTS(
1535                SELECT 1 FROM economic_state_stages
1536                WHERE descriptor_kind = ?1
1537                  AND descriptor_key = ?2
1538            )
1539            "#,
1540            params![ADMISSION_TERMINAL_PROJECTION_DESCRIPTOR_KIND, operation_id],
1541            |row| row.get(0),
1542        )
1543        .map_err(sqlite_error)
1544}
1545
1546fn current_fence_serves_historical(
1547    historical: &StoreMutationFence,
1548    current: &StoreMutationFence,
1549) -> bool {
1550    historical.store_uuid == current.store_uuid
1551        && (historical == current || current.owner_epoch > historical.owner_epoch)
1552}
1553
1554pub(crate) fn finalize_stage_in_transaction(
1555    transaction: &Transaction<'_>,
1556    batch_id: &str,
1557    serving_owner: &SqliteServingOwner,
1558    trusted_now_unix_ms: u64,
1559) -> Result<EconomicStateStageRecord, EconomicStateCacheError> {
1560    validate_digest(batch_id, "batch_id")?;
1561    validate_trusted_time(trusted_now_unix_ms)?;
1562    verify_cache_sql_invariants(transaction)?;
1563    let mut record =
1564        load_stage_tx(transaction, batch_id)?.ok_or(EconomicStateCacheError::NotFound)?;
1565    if record.status == EconomicStateStageStatus::DbFinalized {
1566        return Ok(record);
1567    }
1568    require_transition(record.status, EconomicStateStageStatus::DbFinalized)?;
1569    let committed = record
1570        .committed_view
1571        .as_ref()
1572        .ok_or_else(|| invariant("anchor-advanced stage omitted its committed view"))?;
1573    let mut head_digests = Vec::with_capacity(record.batch.transitions.len());
1574    for transition in &record.batch.transitions {
1575        let head = committed
1576            .heads
1577            .iter()
1578            .find(|head| head.resource_key == transition.resource_key)
1579            .ok_or_else(|| invariant("committed view omitted a staged resource head"))?;
1580        if head != &transition.next_head {
1581            return Err(invariant("committed resource head changed before finalize"));
1582        }
1583        let key_bytes = canonical_json_bytes(&head.resource_key).map_err(canonical_error)?;
1584        let key_digest = sha256_hex(&key_bytes);
1585        let head_bytes = canonical_json_bytes(head).map_err(canonical_error)?;
1586        let head_digest = head.digest()?;
1587        head_digests.push((key_digest.clone(), head_digest.clone()));
1588        transaction
1589            .execute(
1590                r#"
1591                INSERT INTO economic_state_stage_heads (
1592                    batch_id, resource_key_digest, resource_key_json,
1593                    head_digest, head_json
1594                ) VALUES (?1, ?2, ?3, ?4, ?5)
1595                "#,
1596                params![batch_id, &key_digest, &key_bytes, &head_digest, &head_bytes],
1597            )
1598            .map_err(sqlite_error)?;
1599        let checkpoint_sequence =
1600            sqlite_i64(record.batch.checkpoint_sequence, "checkpoint_sequence")?;
1601        let current = transaction
1602            .query_row(
1603                r#"
1604                SELECT resource_key_json, head_digest, head_json,
1605                       checkpoint_sequence, checkpoint_digest, source_batch_id
1606                FROM economic_state_heads WHERE resource_key_digest = ?1
1607                "#,
1608                [&key_digest],
1609                |row| {
1610                    Ok((
1611                        row.get::<_, Vec<u8>>(0)?,
1612                        row.get::<_, String>(1)?,
1613                        row.get::<_, Vec<u8>>(2)?,
1614                        row.get::<_, i64>(3)?,
1615                        row.get::<_, String>(4)?,
1616                        row.get::<_, String>(5)?,
1617                    ))
1618                },
1619            )
1620            .optional()
1621            .map_err(sqlite_error)?;
1622        let publish = match current {
1623            Some((
1624                current_key,
1625                current_head_digest,
1626                current_head,
1627                current_sequence,
1628                current_checkpoint,
1629                current_batch,
1630            )) if current_sequence == checkpoint_sequence => {
1631                if current_key != key_bytes
1632                    || current_head_digest != head_digest
1633                    || current_head != head_bytes
1634                    || current_checkpoint != record.batch.checkpoint_digest
1635                    || current_batch != batch_id
1636                {
1637                    return Err(invariant(
1638                        "equal economic checkpoint has conflicting cached state",
1639                    ));
1640                }
1641                false
1642            }
1643            Some((_, _, _, current_sequence, _, _)) if current_sequence > checkpoint_sequence => {
1644                false
1645            }
1646            _ => true,
1647        };
1648        if publish {
1649            transaction
1650                .execute(
1651                    r#"
1652                INSERT INTO economic_state_heads (
1653                    resource_key_digest, resource_key_json, head_digest,
1654                    head_json, checkpoint_sequence, checkpoint_digest,
1655                    source_batch_id
1656                ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
1657                ON CONFLICT(resource_key_digest) DO UPDATE SET
1658                    resource_key_json = excluded.resource_key_json,
1659                    head_digest = excluded.head_digest,
1660                    head_json = excluded.head_json,
1661                    checkpoint_sequence = excluded.checkpoint_sequence,
1662                    checkpoint_digest = excluded.checkpoint_digest,
1663                    source_batch_id = excluded.source_batch_id
1664                WHERE economic_state_heads.checkpoint_sequence
1665                    < excluded.checkpoint_sequence
1666                "#,
1667                    params![
1668                        &key_digest,
1669                        &key_bytes,
1670                        &head_digest,
1671                        &head_bytes,
1672                        checkpoint_sequence,
1673                        &record.batch.checkpoint_digest,
1674                        batch_id,
1675                    ],
1676                )
1677                .map_err(sqlite_error)?;
1678        }
1679    }
1680    head_digests.sort();
1681    record.status = EconomicStateStageStatus::DbFinalized;
1682    record.version = next_version(record.version)?;
1683    record.updated_at_unix_ms = monotonic_time(&record, trusted_now_unix_ms)?;
1684    record.snapshot_digest = stage_snapshot_digest(&record, &head_digests)?;
1685    update_stage(
1686        transaction,
1687        &record,
1688        None,
1689        None,
1690        EconomicStateStageStatus::EconomicAnchorAdvanced,
1691    )?;
1692    append_stage_commit(transaction, &record, "finalize_stage", serving_owner)?;
1693    Ok(record)
1694}
1695
1696pub(crate) fn initialize_economic_state_cache_schema(
1697    connection: &mut Connection,
1698) -> Result<(), EconomicStateCacheError> {
1699    let on_disk = crate::check_schema_version(
1700        connection,
1701        ECONOMIC_STATE_CACHE_SCHEMA_KEY,
1702        ECONOMIC_STATE_CACHE_SUPPORTED_SCHEMA_VERSION,
1703        ECONOMIC_STATE_CACHE_SCHEMA_ANCHORS,
1704    )
1705    .map_err(|error| invariant(error.to_string()))?;
1706    if on_disk == ECONOMIC_STATE_CACHE_SUPPORTED_SCHEMA_VERSION {
1707        return verify_cache_sql_invariants(connection);
1708    }
1709    let transaction = connection
1710        .transaction_with_behavior(TransactionBehavior::Immediate)
1711        .map_err(sqlite_error)?;
1712    let stage_table_exists = transaction
1713        .query_row(
1714            "SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'economic_state_stages')",
1715            [],
1716            |row| row.get::<_, bool>(0),
1717        )
1718        .map_err(sqlite_error)?;
1719    let descriptor_column_exists = stage_table_exists
1720        && transaction
1721            .query_row(
1722                "SELECT EXISTS(SELECT 1 FROM pragma_table_info('economic_state_stages') WHERE name = 'descriptor_kind')",
1723                [],
1724                |row| row.get::<_, bool>(0),
1725            )
1726            .map_err(sqlite_error)?;
1727    if stage_table_exists && !descriptor_column_exists {
1728        transaction
1729            .execute_batch(ECONOMIC_STATE_CACHE_DESCRIPTOR_MIGRATION)
1730            .map_err(sqlite_error)?;
1731    }
1732    transaction
1733        .execute_batch(ECONOMIC_STATE_CACHE_SCHEMA)
1734        .map_err(sqlite_error)?;
1735    crate::stamp_schema_version(
1736        &transaction,
1737        ECONOMIC_STATE_CACHE_SCHEMA_KEY,
1738        ECONOMIC_STATE_CACHE_SUPPORTED_SCHEMA_VERSION,
1739    )
1740    .map_err(|error| invariant(error.to_string()))?;
1741    verify_cache_sql_invariants(&transaction)?;
1742    transaction.commit().map_err(sqlite_error)
1743}
1744
1745fn qualify_operation_binding(
1746    transaction: &Transaction<'_>,
1747    batch: &EconomicStateBatchV1,
1748    context: Option<EconomicOperationStageContext<'_>>,
1749    active_fence: &StoreMutationFence,
1750    trusted_now_unix_ms: u64,
1751) -> Result<Option<EconomicOperationStageBinding>, EconomicStateCacheError> {
1752    let Some(context) = context else {
1753        if batch.operation_id.is_some() {
1754            return Err(invariant(
1755                "operation-bound batch omitted recovery authority",
1756            ));
1757        }
1758        return Ok(None);
1759    };
1760    let operation = context.operation;
1761    let lease = context.recovery_lease;
1762    operation
1763        .validate()
1764        .map_err(|error| invariant(error.to_string()))?;
1765    if operation.state().is_terminal()
1766        || batch.operation_id.as_deref() != Some(operation.binding().operation_id().as_str())
1767        || lease.operation_id() != operation.binding().operation_id()
1768        || lease.claimed_version() != operation.version()
1769        || lease.coordinator_lease_epoch() != operation.coordinator_lease_epoch()
1770        || lease.store_fence() != active_fence
1771        || trusted_now_unix_ms >= lease.expires_at_unix_ms()
1772        || context
1773            .not_after_unix_ms
1774            .is_some_and(|not_after| trusted_now_unix_ms >= not_after)
1775    {
1776        return Err(EconomicStateCacheError::Fenced);
1777    }
1778    let persisted = transaction
1779        .query_row(
1780            r#"
1781            SELECT operation_json, recovery_claimant_id,
1782                   recovery_coordinator_lease_id, recovery_coordinator_lease_epoch,
1783                   recovery_claimed_version, recovery_expires_at_unix_ms,
1784                   recovery_store_uuid, recovery_store_lease_id,
1785                   recovery_store_owner_epoch
1786            FROM admission_operations WHERE operation_id = ?1 AND terminal = 0
1787            "#,
1788            [operation.binding().operation_id().as_str()],
1789            |row| {
1790                Ok((
1791                    row.get::<_, Vec<u8>>(0)?,
1792                    row.get::<_, Option<String>>(1)?,
1793                    row.get::<_, Option<String>>(2)?,
1794                    row.get::<_, Option<i64>>(3)?,
1795                    row.get::<_, Option<i64>>(4)?,
1796                    row.get::<_, Option<i64>>(5)?,
1797                    row.get::<_, Option<String>>(6)?,
1798                    row.get::<_, Option<String>>(7)?,
1799                    row.get::<_, Option<i64>>(8)?,
1800                ))
1801            },
1802        )
1803        .optional()
1804        .map_err(sqlite_error)?
1805        .ok_or(EconomicStateCacheError::Fenced)?;
1806    let stored: PersistedAdmissionOperationV1 = decode_exact(&persisted.0, "admission operation")?;
1807    let stored = AdmissionOperationV1::from_persisted(stored)
1808        .map_err(|error| invariant(error.to_string()))?;
1809    let exact_claim = persisted.1.as_deref() == Some(lease.claimant_id().as_str())
1810        && persisted.2.as_deref() == Some(lease.coordinator_lease_id().as_str())
1811        && optional_u64(persisted.3, "recovery_coordinator_lease_epoch")?
1812            == Some(lease.coordinator_lease_epoch())
1813        && optional_u64(persisted.4, "recovery_claimed_version")? == Some(lease.claimed_version())
1814        && optional_u64(persisted.5, "recovery_expires_at_unix_ms")?
1815            == Some(lease.expires_at_unix_ms())
1816        && persisted.6.as_deref() == Some(&active_fence.store_uuid)
1817        && persisted.7.as_deref() == Some(&active_fence.lease_id)
1818        && optional_u64(persisted.8, "recovery_store_owner_epoch")?
1819            == Some(active_fence.owner_epoch);
1820    if stored != *operation || !exact_claim {
1821        return Err(EconomicStateCacheError::Fenced);
1822    }
1823    let binding = EconomicOperationStageBinding {
1824        operation_id: operation.binding().operation_id().as_str().to_owned(),
1825        operation_state: operation.state(),
1826        operation_version: operation.version(),
1827        coordinator_lease_epoch: operation.coordinator_lease_epoch(),
1828        coordinator_lease_id: lease.coordinator_lease_id().as_str().to_owned(),
1829        recovery_claimant_id: lease.claimant_id().as_str().to_owned(),
1830        recovery_expires_at_unix_ms: lease.expires_at_unix_ms(),
1831        not_after_unix_ms: context.not_after_unix_ms,
1832        store_fence: active_fence.clone(),
1833    };
1834    binding.validate()?;
1835    Ok(Some(binding))
1836}
1837
1838fn validate_descriptor_text(
1839    value: &str,
1840    maximum: usize,
1841    field: &'static str,
1842) -> Result<(), EconomicStateCacheError> {
1843    if value.is_empty()
1844        || value.len() > maximum
1845        || value.trim() != value
1846        || value.chars().any(char::is_control)
1847    {
1848        Err(invariant(format!("{field} is invalid")))
1849    } else {
1850        Ok(())
1851    }
1852}
1853
1854fn verify_stage_admission_checkpoint(
1855    transaction: &Transaction<'_>,
1856    expected: &EconomicStageAdmissionCheckpoint<'_>,
1857    active_fence: &StoreMutationFence,
1858    trusted_now_unix_ms: u64,
1859) -> Result<(), EconomicStateCacheError> {
1860    validate_descriptor_text(
1861        expected.store_id,
1862        MAX_DESCRIPTOR_KEY_BYTES,
1863        "admission store id",
1864    )?;
1865    validate_digest(expected.digest, "admission commit digest")?;
1866    let current = crate::admission_operation_store::verify_admission_commit_chain(transaction)
1867        .map_err(|error| invariant(error.to_string()))?;
1868    if expected.store_id != active_fence.store_uuid
1869        || expected.sequence != current.head_sequence
1870        || expected.digest != current.chain_digest
1871        || trusted_now_unix_ms < current.trusted_time_high_water_unix_ms
1872    {
1873        return Err(EconomicStateCacheError::Conflict);
1874    }
1875    Ok(())
1876}
1877
1878#[cfg(test)]
1879#[path = "economic_state_cache_tests.rs"]
1880#[allow(clippy::expect_used, clippy::unwrap_used)]
1881mod tests;