1use super::*;
2use chio_core::canonical::canonical_json_bytes;
3use chio_core::crypto::{PublicKey, Signature};
4use chio_core::sha256_hex;
5use hmac::{Hmac, Mac};
6use serde::{Deserialize, Serialize};
7use sha2::Sha256;
8use subtle::ConstantTimeEq;
9
10const BUDGET_ANCHOR_GENESIS_DIGEST: &str =
11 "0000000000000000000000000000000000000000000000000000000000000000";
12
13#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "camelCase", deny_unknown_fields)]
15pub struct BudgetSnapshotAnchorCommitment {
16 pub schema: String,
17 pub commit_sequence: u64,
18 pub previous_chain_digest: String,
19 pub chain_digest: String,
20 pub anchor_set_digest: String,
21 pub leader_url: String,
22 pub election_term: u64,
23 pub committed_at: u64,
24 pub signer_public_key: String,
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct SignedBudgetSnapshotAnchorCommitment {
30 pub body: BudgetSnapshotAnchorCommitment,
31 pub signature: Signature,
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36pub struct BudgetSnapshotAnchorProvenance {
37 pub schema: String,
38 pub chain: Vec<SignedBudgetSnapshotAnchorCommitment>,
39 pub cluster_authenticator: String,
40}
41
42#[derive(Serialize)]
43struct AnchorSetDigestBody<'a> {
44 schema: &'static str,
45 anchors: Vec<AnchorDigestRecord<'a>>,
46}
47
48#[derive(Serialize)]
49struct AnchorDigestRecord<'a> {
50 capability_id: &'a str,
51 grant_index: u32,
52 invocation_count: u32,
53 updated_at: i64,
54 seq: u64,
55 total_cost_exposed: u64,
56 total_cost_realized_spend: u64,
57}
58
59#[derive(Serialize)]
60struct AnchorChainDigestBody<'a> {
61 schema: &'static str,
62 commit_sequence: u64,
63 previous_chain_digest: &'a str,
64 anchor_set_digest: &'a str,
65 leader_url: &'a str,
66 election_term: u64,
67 committed_at: u64,
68 signer_public_key: &'a str,
69}
70
71#[derive(Serialize)]
72struct AnchorAuthenticatorBody<'a> {
73 scheme: &'static str,
74 chain: &'a [SignedBudgetSnapshotAnchorCommitment],
75}
76
77pub fn budget_snapshot_anchor_set_digest(
78 anchors: &[BudgetUsageRecord],
79) -> Result<String, BudgetStoreError> {
80 let mut canonical_anchors = anchors.to_vec();
81 canonical_anchors.sort_by(|left, right| {
82 (&left.capability_id, left.grant_index).cmp(&(&right.capability_id, right.grant_index))
83 });
84 let anchors = canonical_anchors
85 .iter()
86 .map(|anchor| AnchorDigestRecord {
87 capability_id: &anchor.capability_id,
88 grant_index: anchor.grant_index,
89 invocation_count: anchor.invocation_count,
90 updated_at: anchor.updated_at,
91 seq: anchor.seq,
92 total_cost_exposed: anchor.total_cost_exposed,
93 total_cost_realized_spend: anchor.total_cost_realized_spend,
94 })
95 .collect();
96 let bytes = canonical_json_bytes(&AnchorSetDigestBody {
97 schema: "chio.budget-snapshot-anchor-set.v1",
98 anchors,
99 })
100 .map_err(|error| BudgetStoreError::Invariant(error.to_string()))?;
101 Ok(sha256_hex(&bytes))
102}
103
104pub fn budget_snapshot_anchor_chain_digest(
105 body: &BudgetSnapshotAnchorCommitment,
106) -> Result<String, BudgetStoreError> {
107 let bytes = canonical_json_bytes(&AnchorChainDigestBody {
108 schema: "chio.budget-snapshot-anchor-chain.v1",
109 commit_sequence: body.commit_sequence,
110 previous_chain_digest: &body.previous_chain_digest,
111 anchor_set_digest: &body.anchor_set_digest,
112 leader_url: &body.leader_url,
113 election_term: body.election_term,
114 committed_at: body.committed_at,
115 signer_public_key: &body.signer_public_key,
116 })
117 .map_err(|error| BudgetStoreError::Invariant(error.to_string()))?;
118 Ok(sha256_hex(&bytes))
119}
120
121pub fn budget_snapshot_anchor_authenticator(
122 service_token: &str,
123 chain: &[SignedBudgetSnapshotAnchorCommitment],
124) -> Result<String, BudgetStoreError> {
125 let bytes = canonical_json_bytes(&AnchorAuthenticatorBody {
126 scheme: "chio.cluster-budget-anchor-auth.v1",
127 chain,
128 })
129 .map_err(|error| BudgetStoreError::Invariant(error.to_string()))?;
130 let mut authenticator = Hmac::<Sha256>::new_from_slice(service_token.as_bytes())
131 .map_err(|error| BudgetStoreError::Invariant(error.to_string()))?;
132 authenticator.update(&bytes);
133 Ok(hex::encode(authenticator.finalize().into_bytes()))
134}
135
136#[derive(Clone, Debug, PartialEq, Eq)]
137pub struct BudgetStoreSnapshot {
138 pub usages: Vec<BudgetUsageRecord>,
139 pub usage_history_anchors: Vec<BudgetUsageRecord>,
140 pub mutation_events: Vec<BudgetMutationRecord>,
141 pub abandoned_seq_ranges: Vec<(u64, u64)>,
142 pub covered_head: u64,
143 pub origin_ack_heads: Vec<(String, u64)>,
144}
145
146struct BudgetImportBatch<'a> {
147 usages: &'a [BudgetUsageRecord],
148 events: &'a [BudgetMutationRecord],
149 anchors: &'a [BudgetUsageRecord],
150 abandoned_seq_ranges: &'a [(u64, u64)],
151 covered_head: Option<u64>,
152 origin_ack_heads: Option<&'a [(String, u64)]>,
153 allow_verified_anchor_install: bool,
154}
155
156impl SqliteBudgetStore {
157 pub fn budget_import_floor(&self, authority_id: &str) -> Result<u64, BudgetStoreError> {
158 let mut connection = self.connection()?;
159 let transaction = self.begin_read(&mut connection)?;
160 let floor: i64 = transaction
161 .query_row(
162 "SELECT floor_seq FROM budget_import_floors WHERE authority_id = ?1",
163 rusqlite::params![authority_id],
164 |row| row.get(0),
165 )
166 .optional()?
167 .unwrap_or(0);
168 transaction.rollback()?;
169 Ok(floor.max(0) as u64)
170 }
171
172 pub fn record_budget_import_floors(
173 &self,
174 events: &[BudgetMutationRecord],
175 ) -> Result<(), BudgetStoreError> {
176 self.require_standalone_mutation("budget import floor")?;
177 use std::collections::BTreeMap;
178 let mut min_by_origin: BTreeMap<&str, u64> = BTreeMap::new();
179 for event in events {
180 let Some(authority) = event.authority.as_ref() else {
181 continue;
182 };
183 let entry = min_by_origin
184 .entry(authority.authority_id.as_str())
185 .or_insert(event.event_seq);
186 *entry = (*entry).min(event.event_seq);
187 }
188 let mut connection = self.connection()?;
189 let transaction = self.begin_write(&mut connection)?;
190 for (origin, min_seq) in min_by_origin {
191 let floor = min_seq.saturating_sub(1);
192 transaction.execute(
193 "INSERT INTO budget_import_floors (authority_id, floor_seq) VALUES (?1, ?2) \
194 ON CONFLICT(authority_id) DO UPDATE SET floor_seq = MAX(floor_seq, excluded.floor_seq)",
195 rusqlite::params![origin, budget_u64_to_sqlite(floor, "floor_seq")?],
196 )?;
197 }
198 transaction.commit()?;
199 Ok(())
200 }
201
202 pub fn upsert_usage(&self, record: &BudgetUsageRecord) -> Result<(), BudgetStoreError> {
203 self.require_standalone_mutation("budget usage upsert")?;
204 let mut connection = self.connection()?;
205 let transaction = self.begin_write(&mut connection)?;
206 Self::reconcile_imported_usages(&transaction, std::slice::from_ref(record), &[])?;
207 transaction.commit()?;
208 Ok(())
209 }
210
211 pub fn import_snapshot_records(
212 &self,
213 usages: &[BudgetUsageRecord],
214 events: &[BudgetMutationRecord],
215 ) -> Result<(), BudgetStoreError> {
216 self.import_records(
217 &BudgetImportBatch {
218 usages,
219 events,
220 anchors: &[],
221 abandoned_seq_ranges: &[],
222 covered_head: None,
223 origin_ack_heads: None,
224 allow_verified_anchor_install: false,
225 },
226 "budget snapshot import",
227 )
228 }
229
230 pub fn import_snapshot_records_with_anchors(
231 &self,
232 usages: &[BudgetUsageRecord],
233 events: &[BudgetMutationRecord],
234 anchors: &[BudgetUsageRecord],
235 abandoned_seq_ranges: &[(u64, u64)],
236 covered_head: u64,
237 ) -> Result<(), BudgetStoreError> {
238 self.import_records(
239 &BudgetImportBatch {
240 usages,
241 events,
242 anchors,
243 abandoned_seq_ranges,
244 covered_head: Some(covered_head),
245 origin_ack_heads: None,
246 allow_verified_anchor_install: false,
247 },
248 "budget snapshot import",
249 )
250 }
251
252 pub fn import_budget_snapshot(
253 &self,
254 snapshot: &BudgetStoreSnapshot,
255 ) -> Result<(), BudgetStoreError> {
256 const OPERATION: &str = "budget snapshot import";
257 self.require_standalone_mutation(OPERATION)?;
258
259 let validated = Self::validate_budget_snapshot_in_isolation(snapshot)?;
260 let batch = BudgetImportBatch {
261 usages: &validated.usages,
262 events: &validated.mutation_events,
263 anchors: &validated.usage_history_anchors,
264 abandoned_seq_ranges: &validated.abandoned_seq_ranges,
265 covered_head: Some(validated.covered_head),
266 origin_ack_heads: Some(&validated.origin_ack_heads),
267 allow_verified_anchor_install: false,
268 };
269 let mut connection = self.connection()?;
270 let transaction = self.begin_write(&mut connection)?;
271 Self::validate_exact_snapshot_usage_anchors(&transaction, batch.anchors)?;
272 Self::validate_local_snapshot_subset(&transaction, &validated)?;
273 Self::apply_import_batch(&transaction, &batch)?;
274 transaction.commit()?;
275 Ok(())
276 }
277
278 pub fn import_budget_snapshot_with_anchor_provenance(
279 &self,
280 snapshot: &BudgetStoreSnapshot,
281 provenance: &BudgetSnapshotAnchorProvenance,
282 expected_leader_url: &str,
283 expected_election_term: u64,
284 cluster_service_token: &str,
285 ) -> Result<(), BudgetStoreError> {
286 const OPERATION: &str = "verified budget snapshot import";
287 self.require_standalone_mutation(OPERATION)?;
288 let head = verify_budget_snapshot_anchor_provenance(
289 snapshot,
290 provenance,
291 expected_leader_url,
292 expected_election_term,
293 cluster_service_token,
294 )?;
295 let validated = Self::validate_budget_snapshot_in_isolation(snapshot)?;
296 let batch = BudgetImportBatch {
297 usages: &validated.usages,
298 events: &validated.mutation_events,
299 anchors: &validated.usage_history_anchors,
300 abandoned_seq_ranges: &validated.abandoned_seq_ranges,
301 covered_head: Some(validated.covered_head),
302 origin_ack_heads: Some(&validated.origin_ack_heads),
303 allow_verified_anchor_install: true,
304 };
305 let mut connection = self.connection()?;
306 let transaction = self.begin_write(&mut connection)?;
307 verify_local_anchor_provenance_continuity(&transaction, provenance)?;
308 Self::validate_local_snapshot_subset(&transaction, &validated)?;
309 Self::apply_import_batch(&transaction, &batch)?;
310 let local = Self::snapshot_usage_history_anchors(&transaction)?;
311 if local != validated.usage_history_anchors {
312 return Err(BudgetStoreError::Invariant(
313 "verified budget snapshot history anchor set is not exact".to_string(),
314 ));
315 }
316 transaction.execute(
317 r#"
318 INSERT INTO budget_snapshot_anchor_provenance (
319 leader_url, commit_sequence, chain_digest, anchor_set_digest,
320 election_term, signer_public_key, committed_at
321 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
322 ON CONFLICT(leader_url) DO UPDATE SET
323 commit_sequence = excluded.commit_sequence,
324 chain_digest = excluded.chain_digest,
325 anchor_set_digest = excluded.anchor_set_digest,
326 election_term = excluded.election_term,
327 signer_public_key = excluded.signer_public_key,
328 committed_at = excluded.committed_at
329 "#,
330 params![
331 &head.leader_url,
332 budget_u64_to_sqlite(head.commit_sequence, "anchor_commit_sequence")?,
333 &head.chain_digest,
334 &head.anchor_set_digest,
335 budget_u64_to_sqlite(head.election_term, "anchor_election_term")?,
336 &head.signer_public_key,
337 budget_u64_to_sqlite(head.committed_at, "anchor_committed_at")?,
338 ],
339 )?;
340 transaction.commit()?;
341 Ok(())
342 }
343
344 pub fn import_delta_records(
345 &self,
346 usages: &[BudgetUsageRecord],
347 events: &[BudgetMutationRecord],
348 ) -> Result<(), BudgetStoreError> {
349 self.import_records(
350 &BudgetImportBatch {
351 usages,
352 events,
353 anchors: &[],
354 abandoned_seq_ranges: &[],
355 covered_head: None,
356 origin_ack_heads: None,
357 allow_verified_anchor_install: false,
358 },
359 "budget delta import",
360 )
361 }
362
363 fn import_records(
364 &self,
365 batch: &BudgetImportBatch<'_>,
366 operation: &str,
367 ) -> Result<(), BudgetStoreError> {
368 self.require_standalone_mutation(operation)?;
369 Self::validate_import_batch_order(batch.events)?;
370 let mut connection = self.connection()?;
371 let transaction = self.begin_write(&mut connection)?;
372 Self::apply_import_batch(&transaction, batch)?;
373 transaction.commit()?;
374 Ok(())
375 }
376
377 fn apply_import_batch(
378 transaction: &rusqlite::Transaction<'_>,
379 batch: &BudgetImportBatch<'_>,
380 ) -> Result<(), BudgetStoreError> {
381 Self::install_snapshot_usage_anchors(
382 transaction,
383 batch.anchors,
384 batch.allow_verified_anchor_install,
385 )?;
386 for event in batch.events {
387 Self::import_mutation_record_in_transaction(transaction, event)?;
388 }
389 Self::reconcile_imported_usages(transaction, batch.usages, batch.events)?;
390 Self::insert_abandoned_event_seq_ranges(transaction, batch.abandoned_seq_ranges)?;
391 if let Some(claimed_head) = batch.covered_head {
392 let exact_head = Self::contiguous_snapshot_head_from(transaction, 0)?;
393 if claimed_head != exact_head {
394 return Err(BudgetStoreError::Invariant(format!(
395 "budget snapshot claimed covered head {claimed_head}, but imported records prove {exact_head}"
396 )));
397 }
398 let exact_origin_heads = Self::origin_ack_heads_at(transaction, exact_head)?;
399 if let Some(claimed_origin_heads) = batch.origin_ack_heads {
400 if claimed_origin_heads != exact_origin_heads {
401 return Err(BudgetStoreError::Invariant(
402 "budget snapshot origin acknowledgement heads are not proved by the retained mutation prefix"
403 .to_string(),
404 ));
405 }
406 }
407 Self::install_snapshot_coverage(transaction, exact_head, &exact_origin_heads)?;
408 }
409 Ok(())
410 }
411
412 fn validate_budget_snapshot_in_isolation(
413 snapshot: &BudgetStoreSnapshot,
414 ) -> Result<BudgetStoreSnapshot, BudgetStoreError> {
415 let validator = Self::open(":memory:")?;
416 Self::seed_snapshot_validation_anchors(&validator, &snapshot.usage_history_anchors)?;
417 validator.import_records(
418 &BudgetImportBatch {
419 usages: &snapshot.usages,
420 events: &snapshot.mutation_events,
421 anchors: &snapshot.usage_history_anchors,
422 abandoned_seq_ranges: &snapshot.abandoned_seq_ranges,
423 covered_head: Some(snapshot.covered_head),
424 origin_ack_heads: Some(&snapshot.origin_ack_heads),
425 allow_verified_anchor_install: false,
426 },
427 "budget snapshot validation",
428 )?;
429 let validated = validator.export_budget_snapshot()?;
430 Self::validate_staged_snapshot_matches_payload(snapshot, &validated)?;
431 Ok(validated)
432 }
433
434 fn validate_staged_snapshot_matches_payload(
435 payload: &BudgetStoreSnapshot,
436 staged: &BudgetStoreSnapshot,
437 ) -> Result<(), BudgetStoreError> {
438 let mut payload_usages = payload.usages.clone();
439 payload_usages.sort_by(|left, right| {
440 (&left.capability_id, left.grant_index).cmp(&(&right.capability_id, right.grant_index))
441 });
442 if payload_usages != staged.usages {
443 return Err(BudgetStoreError::Invariant(
444 "budget snapshot does not carry its complete final usage projection".to_string(),
445 ));
446 }
447
448 let same_event_history = payload.mutation_events.len() == staged.mutation_events.len()
449 && payload
450 .mutation_events
451 .iter()
452 .zip(&staged.mutation_events)
453 .all(|(payload, staged)| {
454 payload.event_id == staged.event_id && payload.event_seq == staged.event_seq
455 });
456 let same_abandoned_history =
457 payload.abandoned_seq_ranges.iter().all(|&(start, end)| {
458 snapshot_ranges_cover(&staged.abandoned_seq_ranges, start, end)
459 }) && staged.abandoned_seq_ranges.iter().all(|&(start, end)| {
460 snapshot_ranges_cover(&payload.abandoned_seq_ranges, start, end)
461 });
462 if !same_event_history || !same_abandoned_history {
463 return Err(BudgetStoreError::Invariant(
464 "budget snapshot validation changed the supplied durable history".to_string(),
465 ));
466 }
467 Ok(())
468 }
469
470 fn seed_snapshot_validation_anchors(
471 validator: &Self,
472 anchors: &[BudgetUsageRecord],
473 ) -> Result<(), BudgetStoreError> {
474 let mut connection = validator.connection()?;
475 let transaction = validator.begin_write(&mut connection)?;
476 transaction.execute(
477 "INSERT OR IGNORE INTO budget_usage_anchor_migration_gate(singleton) VALUES (1)",
478 [],
479 )?;
480 for anchor in anchors {
481 Self::upsert_usage_in_transaction(&transaction, anchor)?;
482 transaction.execute(
483 r#"
484 INSERT INTO budget_usage_history_anchors (
485 capability_id, grant_index, invocation_count, updated_at, seq,
486 total_cost_exposed, total_cost_realized_spend,
487 anchored_schema_version
488 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 6)
489 "#,
490 params![
491 &anchor.capability_id,
492 i64::from(anchor.grant_index),
493 i64::from(anchor.invocation_count),
494 anchor.updated_at,
495 budget_u64_to_sqlite(anchor.seq, "anchor_seq")?,
496 budget_u64_to_sqlite(anchor.total_cost_exposed, "anchor_total_cost_exposed")?,
497 budget_u64_to_sqlite(
498 anchor.total_cost_realized_spend,
499 "anchor_total_cost_realized_spend"
500 )?,
501 ],
502 )?;
503 }
504 transaction.execute("DELETE FROM budget_usage_anchor_migration_gate", [])?;
505 transaction.commit()?;
506 Ok(())
507 }
508
509 fn validate_exact_snapshot_usage_anchors(
510 transaction: &rusqlite::Transaction<'_>,
511 anchors: &[BudgetUsageRecord],
512 ) -> Result<(), BudgetStoreError> {
513 Self::install_snapshot_usage_anchors(transaction, anchors, false)?;
514 let local = Self::snapshot_usage_history_anchors(transaction)?;
515 let mut incoming = anchors.to_vec();
516 incoming.sort_by(|left, right| {
517 (&left.capability_id, left.grant_index).cmp(&(&right.capability_id, right.grant_index))
518 });
519 if local != incoming {
520 return Err(BudgetStoreError::Invariant(
521 "budget snapshot history anchor set differs from immutable local migration anchors"
522 .to_string(),
523 ));
524 }
525 Ok(())
526 }
527
528 fn validate_local_snapshot_subset(
529 transaction: &rusqlite::Transaction<'_>,
530 snapshot: &BudgetStoreSnapshot,
531 ) -> Result<(), BudgetStoreError> {
532 let incoming_events = snapshot
533 .mutation_events
534 .iter()
535 .map(|event| (event.event_id.as_str(), event))
536 .collect::<std::collections::BTreeMap<_, _>>();
537 for local in Self::snapshot_mutation_events(transaction)? {
538 if incoming_events.get(local.event_id.as_str()).copied() != Some(&local) {
539 return Err(BudgetStoreError::Invariant(format!(
540 "budget snapshot does not retain identical local event `{}`",
541 local.event_id
542 )));
543 }
544 }
545
546 for (start, end) in Self::snapshot_abandoned_seq_ranges(transaction)? {
547 if !snapshot_ranges_cover(&snapshot.abandoned_seq_ranges, start, end) {
548 return Err(BudgetStoreError::Invariant(format!(
549 "budget snapshot does not retain local abandoned sequence range {start}..={end}"
550 )));
551 }
552 }
553
554 for usage in Self::snapshot_usages(transaction)? {
555 if !snapshot_proves_usage(snapshot, &usage) {
556 return Err(BudgetStoreError::Invariant(format!(
557 "budget snapshot does not prove local usage `{}` grant {} at sequence {}",
558 usage.capability_id, usage.grant_index, usage.seq
559 )));
560 }
561 }
562 Ok(())
563 }
564
565 fn install_snapshot_coverage(
566 transaction: &rusqlite::Transaction<'_>,
567 covered_head: u64,
568 origin_ack_heads: &[(String, u64)],
569 ) -> Result<(), BudgetStoreError> {
570 let covered_head = budget_u64_to_sqlite(covered_head, "covered_head")?;
571 transaction.execute(
572 "UPDATE budget_snapshot_coverage SET covered_head = ?1 WHERE singleton = 1",
573 params![covered_head],
574 )?;
575 transaction.execute(
576 "UPDATE budget_ack_head_watermark SET head_seq = ?1 WHERE singleton = 1",
577 params![covered_head],
578 )?;
579 transaction.execute("DELETE FROM budget_origin_ack_heads", [])?;
580 for (authority_id, head_seq) in origin_ack_heads {
581 transaction.execute(
582 "INSERT INTO budget_origin_ack_heads (authority_id, head_seq) VALUES (?1, ?2)",
583 params![
584 authority_id,
585 budget_u64_to_sqlite(*head_seq, "origin_ack_head")?
586 ],
587 )?;
588 }
589 raise_budget_replication_seq_floor(transaction, covered_head as u64)?;
590 Ok(())
591 }
592
593 fn origin_ack_heads_at(
594 connection: &Connection,
595 covered_head: u64,
596 ) -> Result<Vec<(String, u64)>, BudgetStoreError> {
597 let mut statement = connection.prepare(
598 r#"
599 SELECT authority_id, MAX(event_seq)
600 FROM budget_mutation_events
601 WHERE authority_id IS NOT NULL AND event_seq <= ?1
602 GROUP BY authority_id
603 ORDER BY authority_id
604 "#,
605 )?;
606 let rows = statement
607 .query_map(
608 params![budget_u64_to_sqlite(covered_head, "covered_head")?],
609 |row| {
610 Ok((
611 row.get::<_, String>(0)?,
612 budget_u64_from_row(row, 1, "origin_ack_head")?,
613 ))
614 },
615 )?
616 .collect::<Result<Vec<_>, _>>()?;
617 Ok(rows)
618 }
619
620 pub fn export_budget_snapshot(&self) -> Result<BudgetStoreSnapshot, BudgetStoreError> {
621 let mut connection = self.connection()?;
622 let transaction = self.begin_read(&mut connection)?;
623 let usages = Self::snapshot_usages(&transaction)?;
624 let usage_history_anchors = Self::snapshot_usage_history_anchors(&transaction)?;
625 let mutation_events = Self::snapshot_mutation_events(&transaction)?;
626 let abandoned_seq_ranges = Self::snapshot_abandoned_seq_ranges(&transaction)?;
627 let covered_head = Self::contiguous_snapshot_head_in(&transaction)?;
628 let origin_ack_heads = Self::origin_ack_heads_at(&transaction, covered_head)?;
629 transaction.rollback()?;
630 Ok(BudgetStoreSnapshot {
631 usages,
632 usage_history_anchors,
633 mutation_events,
634 abandoned_seq_ranges,
635 covered_head,
636 origin_ack_heads,
637 })
638 }
639
640 fn snapshot_usages(
641 connection: &Connection,
642 ) -> Result<Vec<BudgetUsageRecord>, BudgetStoreError> {
643 let mut statement = connection.prepare(
644 r#"
645 SELECT capability_id, grant_index, invocation_count, updated_at, seq,
646 total_cost_exposed, total_cost_realized_spend
647 FROM capability_grant_budgets
648 ORDER BY capability_id, grant_index
649 "#,
650 )?;
651 let rows = statement
652 .query_map([], record_from_row)?
653 .collect::<Result<Vec<_>, _>>()?;
654 Ok(rows)
655 }
656
657 fn snapshot_usage_history_anchors(
658 connection: &Connection,
659 ) -> Result<Vec<BudgetUsageRecord>, BudgetStoreError> {
660 let mut statement = connection.prepare(
661 r#"
662 SELECT capability_id, grant_index, invocation_count, updated_at, seq,
663 total_cost_exposed, total_cost_realized_spend
664 FROM budget_usage_history_anchors
665 ORDER BY capability_id, grant_index
666 "#,
667 )?;
668 let rows = statement
669 .query_map([], record_from_row)?
670 .collect::<Result<Vec<_>, _>>()?;
671 Ok(rows)
672 }
673
674 fn snapshot_mutation_events(
675 transaction: &rusqlite::Transaction<'_>,
676 ) -> Result<Vec<BudgetMutationRecord>, BudgetStoreError> {
677 let event_ids = {
678 let mut statement = transaction
679 .prepare("SELECT event_id FROM budget_mutation_events ORDER BY event_seq")?;
680 let event_ids = statement
681 .query_map([], |row| row.get::<_, String>(0))?
682 .collect::<Result<Vec<_>, _>>()?;
683 event_ids
684 };
685 event_ids
686 .iter()
687 .map(|event_id| {
688 Self::load_projected_mutation_event(transaction, event_id)?.ok_or_else(|| {
689 BudgetStoreError::Invariant(format!(
690 "budget mutation event `{event_id}` disappeared during snapshot export"
691 ))
692 })
693 })
694 .collect()
695 }
696
697 fn snapshot_abandoned_seq_ranges(
698 connection: &Connection,
699 ) -> Result<Vec<(u64, u64)>, BudgetStoreError> {
700 let mut statement = connection.prepare(
701 r#"
702 SELECT MIN(seq), MAX(seq)
703 FROM (
704 SELECT seq, seq - ROW_NUMBER() OVER (ORDER BY seq) AS island
705 FROM budget_abandoned_event_seqs
706 )
707 GROUP BY island
708 ORDER BY MIN(seq)
709 "#,
710 )?;
711 let rows = statement
712 .query_map([], |row| {
713 Ok((
714 budget_u64_from_row(row, 0, "abandoned_range_start")?,
715 budget_u64_from_row(row, 1, "abandoned_range_end")?,
716 ))
717 })?
718 .collect::<Result<Vec<_>, _>>()?;
719 Ok(rows)
720 }
721
722 pub fn budget_snapshot_covered_head(&self) -> Result<u64, BudgetStoreError> {
723 let mut connection = self.connection()?;
724 let transaction = self.begin_read(&mut connection)?;
725 let head = Self::contiguous_snapshot_head_in(&transaction)?;
726 transaction.rollback()?;
727 Ok(head)
728 }
729
730 pub(super) fn contiguous_snapshot_head_in(
731 connection: &Connection,
732 ) -> Result<u64, BudgetStoreError> {
733 Self::contiguous_snapshot_head_from(connection, 0)
734 }
735
736 pub(super) fn rebuild_snapshot_proof_caches(
737 connection: &mut Connection,
738 ) -> Result<(), BudgetStoreError> {
739 let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?;
740 let covered_head = Self::contiguous_snapshot_head_from(&transaction, 0)?;
741 let origin_ack_heads = Self::origin_ack_heads_at(&transaction, covered_head)?;
742 Self::install_snapshot_coverage(&transaction, covered_head, &origin_ack_heads)?;
743 transaction.commit()?;
744 Ok(())
745 }
746
747 fn contiguous_snapshot_head_from(
748 connection: &Connection,
749 floor: u64,
750 ) -> Result<u64, BudgetStoreError> {
751 let floor_sqlite = budget_u64_to_sqlite(floor, "snapshot_covered_head")?;
752 let next_slot = floor_sqlite.checked_add(1).ok_or_else(|| {
753 BudgetStoreError::Overflow("budget snapshot head overflowed i64".to_string())
754 })?;
755 let next_slot_filled: bool = connection.query_row(
756 r#"
757 SELECT EXISTS(
758 SELECT 1 FROM budget_mutation_events WHERE event_seq = ?1
759 UNION ALL
760 SELECT 1 FROM budget_abandoned_event_seqs WHERE seq = ?1
761 )
762 "#,
763 params![next_slot],
764 |row| row.get::<_, i64>(0).map(|value| value != 0),
765 )?;
766 if !next_slot_filled {
767 return Ok(floor);
768 }
769 let head: i64 = connection.query_row(
770 r#"
771 WITH filled AS (
772 SELECT event_seq AS seq
773 FROM budget_mutation_events
774 WHERE event_seq IS NOT NULL AND event_seq > ?1
775 UNION
776 SELECT seq
777 FROM budget_abandoned_event_seqs
778 WHERE seq > ?1
779 ),
780 run AS (
781 SELECT seq, seq - ROW_NUMBER() OVER (ORDER BY seq) AS island
782 FROM filled
783 )
784 SELECT COALESCE(MAX(seq), ?1)
785 FROM run
786 WHERE island = ?1
787 "#,
788 params![floor_sqlite],
789 |row| row.get(0),
790 )?;
791 u64::try_from(head).map_err(|_| {
792 BudgetStoreError::Invariant("budget snapshot head has a negative sequence".to_string())
793 })
794 }
795
796 pub fn list_usage_history_anchors(&self) -> Result<Vec<BudgetUsageRecord>, BudgetStoreError> {
797 let mut connection = self.connection()?;
798 let transaction = self.begin_read(&mut connection)?;
799 let anchors = {
800 let mut statement = transaction.prepare(
801 r#"
802 SELECT capability_id, grant_index, invocation_count, updated_at, seq,
803 total_cost_exposed, total_cost_realized_spend
804 FROM budget_usage_history_anchors
805 ORDER BY capability_id, grant_index
806 "#,
807 )?;
808 let rows = statement
809 .query_map([], record_from_row)?
810 .collect::<Result<Vec<_>, _>>()?;
811 rows
812 };
813 transaction.rollback()?;
814 Ok(anchors)
815 }
816}
817
818fn snapshot_ranges_cover(ranges: &[(u64, u64)], start: u64, end: u64) -> bool {
819 let mut next = start;
820 for &(range_start, range_end) in ranges {
821 if range_end < next {
822 continue;
823 }
824 if range_start > next {
825 return false;
826 }
827 if range_end >= end {
828 return true;
829 }
830 let Some(after_range) = range_end.checked_add(1) else {
831 return true;
832 };
833 next = after_range;
834 }
835 false
836}
837
838fn snapshot_proves_usage(snapshot: &BudgetStoreSnapshot, usage: &BudgetUsageRecord) -> bool {
839 snapshot
840 .usage_history_anchors
841 .iter()
842 .any(|anchor| anchor == usage)
843 || snapshot.mutation_events.iter().any(|event| {
844 event.capability_id == usage.capability_id
845 && event.grant_index == usage.grant_index
846 && event.event_seq == usage.seq
847 && event.usage_seq == Some(event.event_seq)
848 && event.recorded_at == usage.updated_at
849 && event.invocation_count_after == usage.invocation_count
850 && event.total_cost_exposed_after == usage.total_cost_exposed
851 && event.total_cost_realized_spend_after == usage.total_cost_realized_spend
852 })
853}
854
855fn verify_budget_snapshot_anchor_provenance<'a>(
856 snapshot: &BudgetStoreSnapshot,
857 provenance: &'a BudgetSnapshotAnchorProvenance,
858 expected_leader_url: &str,
859 expected_election_term: u64,
860 cluster_service_token: &str,
861) -> Result<&'a BudgetSnapshotAnchorCommitment, BudgetStoreError> {
862 if expected_leader_url.is_empty()
863 || expected_election_term == 0
864 || cluster_service_token.is_empty()
865 || provenance.chain.is_empty()
866 || provenance.schema != "chio.budget-snapshot-anchor-provenance.v1"
867 {
868 return Err(BudgetStoreError::Invariant(
869 "budget snapshot anchor provenance trust context is incomplete".to_string(),
870 ));
871 }
872 let expected_authenticator =
873 budget_snapshot_anchor_authenticator(cluster_service_token, &provenance.chain)?;
874 if !bool::from(
875 provenance
876 .cluster_authenticator
877 .as_bytes()
878 .ct_eq(expected_authenticator.as_bytes()),
879 ) {
880 return Err(BudgetStoreError::Invariant(
881 "budget snapshot anchor provenance cluster authentication failed".to_string(),
882 ));
883 }
884 let expected_anchor_digest =
885 budget_snapshot_anchor_set_digest(&snapshot.usage_history_anchors)?;
886 let mut previous_digest = BUDGET_ANCHOR_GENESIS_DIGEST;
887 for (index, signed) in provenance.chain.iter().enumerate() {
888 let expected_sequence = u64::try_from(index)
889 .ok()
890 .and_then(|value| value.checked_add(1))
891 .ok_or_else(|| {
892 BudgetStoreError::Invariant(
893 "budget snapshot anchor provenance sequence overflowed".to_string(),
894 )
895 })?;
896 let body = &signed.body;
897 if body.schema != "chio.budget-snapshot-anchor-commitment.v1"
898 || body.commit_sequence != expected_sequence
899 || body.previous_chain_digest != previous_digest
900 || body.election_term == 0
901 || body.leader_url.is_empty()
902 || body.signer_public_key.is_empty()
903 || budget_snapshot_anchor_chain_digest(body)? != body.chain_digest
904 {
905 return Err(BudgetStoreError::Invariant(
906 "budget snapshot anchor provenance chain is invalid".to_string(),
907 ));
908 }
909 let signer = PublicKey::from_hex(&body.signer_public_key)
910 .map_err(|error| BudgetStoreError::Invariant(error.to_string()))?;
911 if !signer
912 .verify_canonical(body, &signed.signature)
913 .map_err(|error| BudgetStoreError::Invariant(error.to_string()))?
914 {
915 return Err(BudgetStoreError::Invariant(
916 "budget snapshot anchor provenance signature is invalid".to_string(),
917 ));
918 }
919 previous_digest = &body.chain_digest;
920 }
921 let head = provenance.chain.last().ok_or_else(|| {
922 BudgetStoreError::Invariant("budget snapshot anchor provenance is empty".to_string())
923 })?;
924 if head.body.leader_url != expected_leader_url
925 || head.body.election_term != expected_election_term
926 || head.body.anchor_set_digest != expected_anchor_digest
927 {
928 return Err(BudgetStoreError::Invariant(
929 "budget snapshot anchor provenance is not bound to the elected leader, term, and exact anchor set"
930 .to_string(),
931 ));
932 }
933 Ok(&head.body)
934}
935
936fn verify_local_anchor_provenance_continuity(
937 transaction: &rusqlite::Transaction<'_>,
938 provenance: &BudgetSnapshotAnchorProvenance,
939) -> Result<(), BudgetStoreError> {
940 let head = provenance.chain.last().ok_or_else(|| {
941 BudgetStoreError::Invariant("budget snapshot anchor provenance is empty".to_string())
942 })?;
943 let local = transaction
944 .query_row(
945 r#"
946 SELECT commit_sequence, chain_digest
947 FROM budget_snapshot_anchor_provenance
948 WHERE leader_url = ?1
949 "#,
950 params![&head.body.leader_url],
951 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
952 )
953 .optional()?;
954 let Some((local_sequence, local_digest)) = local else {
955 return Ok(());
956 };
957 let local_sequence = local_sequence.max(0) as u64;
958 let index = usize::try_from(local_sequence.saturating_sub(1)).map_err(|_| {
959 BudgetStoreError::Invariant(
960 "persisted budget snapshot anchor provenance sequence is invalid".to_string(),
961 )
962 })?;
963 let committed = provenance.chain.get(index).ok_or_else(|| {
964 BudgetStoreError::Invariant(
965 "budget snapshot anchor provenance rewinds the persisted leader chain".to_string(),
966 )
967 })?;
968 if committed.body.commit_sequence != local_sequence
969 || committed.body.chain_digest != local_digest
970 {
971 return Err(BudgetStoreError::Invariant(
972 "budget snapshot anchor provenance forks the persisted leader chain".to_string(),
973 ));
974 }
975 Ok(())
976}