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(¤t_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;