1use super::*;
2
3const RECEIPT_STORE_LEGACY_ANCHOR_TABLES: &[&str] =
18 &["chio_tool_receipts", "http_receipts", "tool_receipts"];
19
20const RECEIPT_STORE_GENERIC_LEGACY_ANCHOR_TABLES: &[&str] = &["http_receipts", "tool_receipts"];
26
27fn configure_sqlite_connection(
28 connection: &mut Connection,
29 max_page_count: Option<u32>,
30) -> Result<(), ReceiptStoreError> {
31 connection.execute_batch(
32 r#"
33 PRAGMA journal_mode = WAL;
34 PRAGMA synchronous = FULL;
35 PRAGMA busy_timeout = 5000;
36 PRAGMA foreign_keys = ON;
37 PRAGMA auto_vacuum = INCREMENTAL;
38 "#,
39 )?;
40 assert_sqlite_durability_pragmas(connection)?;
41 if let Some(max_page_count) = max_page_count {
48 connection.pragma_update(None, "max_page_count", max_page_count)?;
49 let effective: i64 = connection.query_row("PRAGMA max_page_count", [], |row| row.get(0))?;
57 if effective != i64::from(max_page_count) {
58 return Err(ReceiptStoreError::Conflict(format!(
59 "sqlite max_page_count requested {max_page_count} but effective limit is {effective}"
60 )));
61 }
62 }
63 Ok(())
64}
65
66fn assert_sqlite_durability_pragmas(connection: &Connection) -> Result<(), ReceiptStoreError> {
67 let journal_mode: String = connection.query_row("PRAGMA journal_mode", [], |row| row.get(0))?;
68 if !journal_mode.eq_ignore_ascii_case("wal") {
69 return Err(ReceiptStoreError::Conflict(format!(
70 "sqlite receipt store journal_mode must be WAL, got {journal_mode}"
71 )));
72 }
73
74 let synchronous: i64 = connection.query_row("PRAGMA synchronous", [], |row| row.get(0))?;
75 if synchronous != 2 {
76 return Err(ReceiptStoreError::Conflict(format!(
77 "sqlite receipt store synchronous must be FULL, got {synchronous}"
78 )));
79 }
80
81 let busy_timeout: i64 = connection.query_row("PRAGMA busy_timeout", [], |row| row.get(0))?;
82 if busy_timeout < 5000 {
83 return Err(ReceiptStoreError::Conflict(format!(
84 "sqlite receipt store busy_timeout must be at least 5000ms, got {busy_timeout}"
85 )));
86 }
87
88 let foreign_keys: i64 = connection.query_row("PRAGMA foreign_keys", [], |row| row.get(0))?;
89 if foreign_keys != 1 {
90 return Err(ReceiptStoreError::Conflict(format!(
91 "sqlite receipt store foreign_keys must be ON, got {foreign_keys}"
92 )));
93 }
94
95 Ok(())
96}
97
98fn settlement_store_binding_if_ready(
99 connection: &Connection,
100) -> Result<Option<chio_settle::SettlementStoreBinding>, ReceiptStoreError> {
101 if !settlement_projection_schema_is_installed(connection)? {
102 return Ok(None);
103 }
104 let mut digest = [0u8; 32];
105 OsRng.try_fill_bytes(&mut digest).map_err(|error| {
106 ReceiptStoreError::Io(std::io::Error::other(format!(
107 "failed to generate settlement store binding: {error}"
108 )))
109 })?;
110 Ok(Some(chio_settle::SettlementStoreBinding::from_digest(
111 digest,
112 )))
113}
114
115fn settlement_projection_schema_is_installed(
116 connection: &Connection,
117) -> Result<bool, ReceiptStoreError> {
118 let reference = Connection::open_in_memory()?;
119 reference.execute_batch(crate::dead_letters::SETTLE_DEAD_LETTERS_MIGRATION)?;
120 reference.execute_batch(crate::settle_attempts::SETTLE_ATTEMPTS_MIGRATION)?;
121 Ok(settlement_schema_manifest(connection)? == settlement_schema_manifest(&reference)?)
122}
123
124#[derive(Debug, PartialEq, Eq)]
125struct SettlementSchemaObject {
126 object_type: String,
127 name: String,
128 table_name: String,
129 sql: String,
130}
131
132fn settlement_schema_manifest(
133 connection: &Connection,
134) -> Result<Vec<SettlementSchemaObject>, ReceiptStoreError> {
135 let mut statement = connection.prepare(
136 "SELECT type, name, tbl_name, sql FROM sqlite_master \
137 WHERE name IN (\
138 'settle_attempts', \
139 'idx_settle_attempts_visible', \
140 'settle_dead_letters', \
141 'idx_settle_dead_letters_finalized_at', \
142 'trg_settle_attempts_reject_terminal_insert', \
143 'trg_settle_dead_letters_reject_attempt_insert'\
144 ) OR (\
145 type = 'trigger' AND \
146 tbl_name IN ('settle_attempts', 'settle_dead_letters')\
147 ) ORDER BY type ASC, name ASC",
148 )?;
149 let manifest = statement
150 .query_map([], |row| {
151 Ok(SettlementSchemaObject {
152 object_type: row.get(0)?,
153 name: row.get(1)?,
154 table_name: row.get(2)?,
155 sql: row.get(3)?,
156 })
157 })?
158 .collect::<Result<Vec<_>, _>>()
159 .map_err(ReceiptStoreError::from)?;
160 Ok(manifest)
161}
162
163impl SqliteReceiptStore {
164 pub fn open(path: impl AsRef<Path>) -> Result<Self, ReceiptStoreError> {
165 Self::open_with_pool_config(path, crate::SqlitePoolConfig::default())
166 }
167
168 pub fn wait_for_writer_ready(&self, timeout: Duration) -> Result<(), ReceiptStoreError> {
172 self.receipt_commit_actor.flush_with_timeout(timeout)?;
173 if !self.writer_serving_closed() {
174 return Ok(());
175 }
176 let detail = self
177 .receipt_commit_actor
178 .writer_counters()
179 .last_error
180 .unwrap_or_else(|| "durable receipt head is unavailable".to_string());
181 Err(ReceiptStoreError::Conflict(format!(
182 "receipt commit writer failed startup readiness: {detail}"
183 )))
184 }
185
186 pub fn open_existing(path: impl AsRef<Path>) -> Result<Self, ReceiptStoreError> {
187 Self::open_existing_with_pool_config(path, crate::SqlitePoolConfig::default())
188 }
189
190 pub fn open_with_pool_config(
191 path: impl AsRef<Path>,
192 pool_config: crate::SqlitePoolConfig,
193 ) -> Result<Self, ReceiptStoreError> {
194 Self::open_with_options(
195 path,
196 crate::SqliteStoreOptions {
197 pool: pool_config,
198 ..crate::SqliteStoreOptions::default()
199 },
200 )
201 }
202
203 fn open_existing_with_pool_config(
204 path: impl AsRef<Path>,
205 pool_config: crate::SqlitePoolConfig,
206 ) -> Result<Self, ReceiptStoreError> {
207 Self::open_existing_with_options(
208 path,
209 crate::SqliteStoreOptions {
210 pool: pool_config,
211 ..crate::SqliteStoreOptions::default()
212 },
213 )
214 }
215
216 pub fn open_with_options(
217 path: impl AsRef<Path>,
218 options: crate::SqliteStoreOptions,
219 ) -> Result<Self, ReceiptStoreError> {
220 Self::open_with_pool_config_and_flags(path, options, true)
221 }
222
223 pub fn open_existing_with_options(
224 path: impl AsRef<Path>,
225 options: crate::SqliteStoreOptions,
226 ) -> Result<Self, ReceiptStoreError> {
227 Self::open_with_pool_config_and_flags(path, options, false)
228 }
229
230 fn open_with_pool_config_and_flags(
231 path: impl AsRef<Path>,
232 options: crate::SqliteStoreOptions,
233 create_if_missing: bool,
234 ) -> Result<Self, ReceiptStoreError> {
235 let path = path.as_ref();
236 let connection_flags = if create_if_missing {
237 None
238 } else {
239 Some(existing_database_open_flags())
240 };
241
242 if create_if_missing {
243 if let Some(parent) = crate::sqlite_parent_dir_to_create(path) {
247 fs::create_dir_all(&parent)?;
248 }
249 }
250
251 let mut connection = match connection_flags {
252 Some(flags) => Connection::open_with_flags(path, flags).map_err(|error| {
253 if error.sqlite_error_code() == Some(rusqlite::ErrorCode::CannotOpen) {
254 ReceiptStoreError::NotFound(format!(
255 "receipt database {} does not exist",
256 path.display()
257 ))
258 } else {
259 ReceiptStoreError::Sqlite(error)
260 }
261 })?,
262 None => Connection::open(path)?,
263 };
264 if !create_if_missing {
265 require_existing_receipt_schema(path, &connection)?;
266 let on_disk_schema_version = crate::check_schema_version(
270 &connection,
271 RECEIPT_STORE_SCHEMA_KEY,
272 RECEIPT_STORE_SUPPORTED_SCHEMA_VERSION,
273 RECEIPT_STORE_LEGACY_ANCHOR_TABLES,
274 )
275 .map_err(|error| ReceiptStoreError::Conflict(error.to_string()))?;
276 if on_disk_schema_version < RECEIPT_STORE_SUPPORTED_SCHEMA_VERSION {
277 return Err(ReceiptStoreError::Conflict(format!(
278 "receipt database schema version {on_disk_schema_version} requires writable migration to version {RECEIPT_STORE_SUPPORTED_SCHEMA_VERSION}; reopen it with SqliteReceiptStore::open"
279 )));
280 }
281 configure_sqlite_connection(&mut connection, options.pool.max_page_count)?;
282 super::support::ensure_transparency_projection_guards(&connection)?;
283 verify_receipt_cost_projection(&connection)?;
284 let settlement_store_binding = settlement_store_binding_if_ready(&connection)?;
285 drop(connection);
286
287 let reader_pool = build_receipt_pool(
288 path,
289 options.pool.reader_pool_max_size,
290 "reader",
291 connection_flags,
292 options.pool.max_page_count,
293 )?;
294 let writer_pool = build_receipt_pool(
295 path,
296 options.pool.writer_pool_max_size,
297 "writer",
298 connection_flags,
299 options.pool.max_page_count,
300 )?;
301
302 return Ok(Self {
303 receipt_commit_actor: ReceiptCommitActor::start(
304 writer_pool,
305 options.incremental_verification,
306 ),
307 pool: reader_pool,
308 settlement_store_binding,
309 strict_tenant_isolation: std::sync::atomic::AtomicBool::new(true),
310 incremental_verification: options.incremental_verification,
311 });
312 }
313
314 reject_foreign_legacy_receipt_anchor(&connection)?;
321 let on_disk_schema_version = crate::check_schema_version(
322 &connection,
323 RECEIPT_STORE_SCHEMA_KEY,
324 RECEIPT_STORE_SUPPORTED_SCHEMA_VERSION,
325 RECEIPT_STORE_LEGACY_ANCHOR_TABLES,
326 )
327 .map_err(|error| ReceiptStoreError::Conflict(error.to_string()))?;
328 configure_sqlite_connection(&mut connection, options.pool.max_page_count)?;
329 let schema_migration =
330 connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
331 schema_migration.execute_batch(
332 r#"
333 CREATE TABLE IF NOT EXISTS chio_tool_receipts (
334 seq INTEGER PRIMARY KEY AUTOINCREMENT,
335 receipt_id TEXT NOT NULL UNIQUE,
336 timestamp INTEGER NOT NULL,
337 capability_id TEXT NOT NULL,
338 subject_key TEXT,
339 issuer_key TEXT,
340 grant_index INTEGER,
341 tool_server TEXT NOT NULL,
342 tool_name TEXT NOT NULL,
343 decision_kind TEXT NOT NULL,
344 policy_hash TEXT NOT NULL,
345 content_hash TEXT NOT NULL,
346 raw_json TEXT NOT NULL,
347 cost_currency TEXT CHECK (
348 cost_currency IS NULL OR (
349 typeof(cost_currency) = 'text' AND
350 length(cost_currency) = 3 AND
351 cost_currency NOT GLOB '*[^A-Z]*'
352 )
353 ),
354 cost_charged_be BLOB CHECK (
355 (cost_currency IS NULL AND cost_charged_be IS NULL) OR
356 (
357 cost_currency IS NOT NULL AND
358 typeof(cost_charged_be) = 'blob' AND
359 length(cost_charged_be) = 8
360 )
361 )
362 );
363
364 CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_timestamp
365 ON chio_tool_receipts(timestamp);
366 CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_capability
367 ON chio_tool_receipts(capability_id);
368 CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_subject
369 ON chio_tool_receipts(subject_key);
370 CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_grant
371 ON chio_tool_receipts(capability_id, grant_index);
372 CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_tool
373 ON chio_tool_receipts(tool_server, tool_name);
374 CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_decision
375 ON chio_tool_receipts(decision_kind);
376
377 CREATE TABLE IF NOT EXISTS chio_authorization_receipt_consumptions (
378 authorization_receipt_id TEXT PRIMARY KEY REFERENCES chio_tool_receipts(receipt_id) ON DELETE RESTRICT,
379 consumer_receipt_id TEXT NOT NULL UNIQUE,
380 request_id TEXT NOT NULL,
381 session_id TEXT NOT NULL,
382 tool_call_id TEXT NOT NULL,
383 -- tenant_id may be NULL for non-enterprise / single-tenant
384 -- deployments where the authorization receipt itself carries
385 -- `tenant_id: None`. The consumption record mirrors the
386 -- receipt's tenant scope verbatim.
387 tenant_id TEXT,
388 parameter_hash TEXT NOT NULL,
389 consumed_at_unix_ms INTEGER NOT NULL
390 );
391 CREATE INDEX IF NOT EXISTS idx_chio_authorization_consumptions_consumer
392 ON chio_authorization_receipt_consumptions(consumer_receipt_id);
393 CREATE INDEX IF NOT EXISTS idx_chio_authorization_consumptions_scope
394 ON chio_authorization_receipt_consumptions(session_id, tool_call_id, request_id);
395
396 CREATE TABLE IF NOT EXISTS settlement_reconciliations (
397 receipt_id TEXT PRIMARY KEY REFERENCES chio_tool_receipts(receipt_id) ON DELETE CASCADE,
398 reconciliation_state TEXT NOT NULL,
399 note TEXT,
400 updated_at INTEGER NOT NULL
401 );
402 CREATE INDEX IF NOT EXISTS idx_settlement_reconciliations_updated_at
403 ON settlement_reconciliations(updated_at);
404
405 CREATE TABLE IF NOT EXISTS metered_billing_reconciliations (
406 receipt_id TEXT PRIMARY KEY REFERENCES chio_tool_receipts(receipt_id) ON DELETE CASCADE,
407 adapter_kind TEXT NOT NULL,
408 evidence_id TEXT NOT NULL,
409 observed_units INTEGER NOT NULL,
410 billed_cost_units INTEGER NOT NULL,
411 billed_cost_currency TEXT NOT NULL,
412 evidence_sha256 TEXT,
413 recorded_at INTEGER NOT NULL,
414 reconciliation_state TEXT NOT NULL,
415 note TEXT,
416 updated_at INTEGER NOT NULL,
417 UNIQUE (adapter_kind, evidence_id)
418 );
419 CREATE INDEX IF NOT EXISTS idx_metered_billing_reconciliations_updated_at
420 ON metered_billing_reconciliations(updated_at);
421
422 CREATE TABLE IF NOT EXISTS underwriting_decisions (
423 decision_id TEXT PRIMARY KEY,
424 issued_at INTEGER NOT NULL,
425 capability_id TEXT,
426 subject_key TEXT,
427 tool_server TEXT,
428 tool_name TEXT,
429 outcome TEXT NOT NULL,
430 lifecycle_state TEXT NOT NULL,
431 review_state TEXT NOT NULL,
432 risk_class TEXT NOT NULL,
433 supersedes_decision_id TEXT REFERENCES underwriting_decisions(decision_id),
434 superseded_by_decision_id TEXT REFERENCES underwriting_decisions(decision_id),
435 premium_units INTEGER,
436 raw_json TEXT NOT NULL,
437 signer_key TEXT NOT NULL,
438 signature TEXT NOT NULL
439 );
440 CREATE INDEX IF NOT EXISTS idx_underwriting_decisions_issued_at
441 ON underwriting_decisions(issued_at);
442 CREATE INDEX IF NOT EXISTS idx_underwriting_decisions_capability
443 ON underwriting_decisions(capability_id);
444 CREATE INDEX IF NOT EXISTS idx_underwriting_decisions_subject
445 ON underwriting_decisions(subject_key);
446 CREATE INDEX IF NOT EXISTS idx_underwriting_decisions_tool
447 ON underwriting_decisions(tool_server, tool_name);
448 CREATE INDEX IF NOT EXISTS idx_underwriting_decisions_outcome
449 ON underwriting_decisions(outcome);
450 CREATE INDEX IF NOT EXISTS idx_underwriting_decisions_lifecycle
451 ON underwriting_decisions(lifecycle_state);
452
453 CREATE TABLE IF NOT EXISTS underwriting_appeals (
454 appeal_id TEXT PRIMARY KEY,
455 decision_id TEXT NOT NULL REFERENCES underwriting_decisions(decision_id) ON DELETE CASCADE,
456 requested_by TEXT NOT NULL,
457 reason TEXT NOT NULL,
458 status TEXT NOT NULL,
459 note TEXT,
460 created_at INTEGER NOT NULL,
461 updated_at INTEGER NOT NULL,
462 resolved_by TEXT,
463 replacement_decision_id TEXT REFERENCES underwriting_decisions(decision_id)
464 );
465 CREATE INDEX IF NOT EXISTS idx_underwriting_appeals_decision
466 ON underwriting_appeals(decision_id);
467 CREATE INDEX IF NOT EXISTS idx_underwriting_appeals_status
468 ON underwriting_appeals(status);
469 CREATE INDEX IF NOT EXISTS idx_underwriting_appeals_updated_at
470 ON underwriting_appeals(updated_at);
471
472 CREATE TABLE IF NOT EXISTS credit_facilities (
473 facility_id TEXT PRIMARY KEY,
474 issued_at INTEGER NOT NULL,
475 expires_at INTEGER NOT NULL,
476 capability_id TEXT,
477 subject_key TEXT,
478 tool_server TEXT,
479 tool_name TEXT,
480 disposition TEXT NOT NULL,
481 lifecycle_state TEXT NOT NULL,
482 supersedes_facility_id TEXT REFERENCES credit_facilities(facility_id),
483 superseded_by_facility_id TEXT REFERENCES credit_facilities(facility_id),
484 raw_json TEXT NOT NULL,
485 signer_key TEXT NOT NULL,
486 signature TEXT NOT NULL
487 );
488 CREATE INDEX IF NOT EXISTS idx_credit_facilities_issued_at
489 ON credit_facilities(issued_at);
490 CREATE INDEX IF NOT EXISTS idx_credit_facilities_expires_at
491 ON credit_facilities(expires_at);
492 CREATE INDEX IF NOT EXISTS idx_credit_facilities_capability
493 ON credit_facilities(capability_id);
494 CREATE INDEX IF NOT EXISTS idx_credit_facilities_subject
495 ON credit_facilities(subject_key);
496 CREATE INDEX IF NOT EXISTS idx_credit_facilities_tool
497 ON credit_facilities(tool_server, tool_name);
498 CREATE INDEX IF NOT EXISTS idx_credit_facilities_disposition
499 ON credit_facilities(disposition);
500 CREATE INDEX IF NOT EXISTS idx_credit_facilities_lifecycle
501 ON credit_facilities(lifecycle_state);
502
503 CREATE TABLE IF NOT EXISTS credit_bonds (
504 bond_id TEXT PRIMARY KEY,
505 issued_at INTEGER NOT NULL,
506 expires_at INTEGER NOT NULL,
507 facility_id TEXT,
508 capability_id TEXT,
509 subject_key TEXT,
510 tool_server TEXT,
511 tool_name TEXT,
512 disposition TEXT NOT NULL,
513 lifecycle_state TEXT NOT NULL,
514 supersedes_bond_id TEXT REFERENCES credit_bonds(bond_id),
515 superseded_by_bond_id TEXT REFERENCES credit_bonds(bond_id),
516 raw_json TEXT NOT NULL,
517 signer_key TEXT NOT NULL,
518 signature TEXT NOT NULL
519 );
520 CREATE INDEX IF NOT EXISTS idx_credit_bonds_issued_at
521 ON credit_bonds(issued_at);
522 CREATE INDEX IF NOT EXISTS idx_credit_bonds_expires_at
523 ON credit_bonds(expires_at);
524 CREATE INDEX IF NOT EXISTS idx_credit_bonds_facility
525 ON credit_bonds(facility_id);
526 CREATE INDEX IF NOT EXISTS idx_credit_bonds_capability
527 ON credit_bonds(capability_id);
528 CREATE INDEX IF NOT EXISTS idx_credit_bonds_subject
529 ON credit_bonds(subject_key);
530 CREATE INDEX IF NOT EXISTS idx_credit_bonds_tool
531 ON credit_bonds(tool_server, tool_name);
532 CREATE INDEX IF NOT EXISTS idx_credit_bonds_disposition
533 ON credit_bonds(disposition);
534 CREATE INDEX IF NOT EXISTS idx_credit_bonds_lifecycle
535 ON credit_bonds(lifecycle_state);
536
537 CREATE TABLE IF NOT EXISTS liability_providers (
538 provider_record_id TEXT PRIMARY KEY,
539 issued_at INTEGER NOT NULL,
540 provider_id TEXT NOT NULL,
541 lifecycle_state TEXT NOT NULL,
542 supersedes_provider_record_id TEXT REFERENCES liability_providers(provider_record_id),
543 superseded_by_provider_record_id TEXT REFERENCES liability_providers(provider_record_id),
544 raw_json TEXT NOT NULL,
545 signer_key TEXT NOT NULL,
546 signature TEXT NOT NULL
547 );
548 CREATE INDEX IF NOT EXISTS idx_liability_providers_issued_at
549 ON liability_providers(issued_at);
550 CREATE INDEX IF NOT EXISTS idx_liability_providers_provider_id
551 ON liability_providers(provider_id);
552 CREATE INDEX IF NOT EXISTS idx_liability_providers_lifecycle
553 ON liability_providers(lifecycle_state);
554
555 CREATE TABLE IF NOT EXISTS liability_quote_requests (
556 quote_request_id TEXT PRIMARY KEY,
557 issued_at INTEGER NOT NULL,
558 provider_id TEXT NOT NULL,
559 jurisdiction TEXT NOT NULL,
560 coverage_class TEXT NOT NULL,
561 currency TEXT NOT NULL,
562 subject_key TEXT NOT NULL,
563 raw_json TEXT NOT NULL,
564 signer_key TEXT NOT NULL,
565 signature TEXT NOT NULL
566 );
567 CREATE INDEX IF NOT EXISTS idx_liability_quote_requests_issued_at
568 ON liability_quote_requests(issued_at);
569 CREATE INDEX IF NOT EXISTS idx_liability_quote_requests_provider
570 ON liability_quote_requests(provider_id);
571 CREATE INDEX IF NOT EXISTS idx_liability_quote_requests_subject
572 ON liability_quote_requests(subject_key);
573
574 CREATE TABLE IF NOT EXISTS liability_quote_responses (
575 quote_response_id TEXT PRIMARY KEY,
576 issued_at INTEGER NOT NULL,
577 quote_request_id TEXT NOT NULL REFERENCES liability_quote_requests(quote_request_id),
578 provider_id TEXT NOT NULL,
579 disposition TEXT NOT NULL,
580 expires_at INTEGER,
581 supersedes_quote_response_id TEXT REFERENCES liability_quote_responses(quote_response_id),
582 superseded_by_quote_response_id TEXT REFERENCES liability_quote_responses(quote_response_id),
583 raw_json TEXT NOT NULL,
584 signer_key TEXT NOT NULL,
585 signature TEXT NOT NULL
586 );
587 CREATE INDEX IF NOT EXISTS idx_liability_quote_responses_issued_at
588 ON liability_quote_responses(issued_at);
589 CREATE INDEX IF NOT EXISTS idx_liability_quote_responses_request
590 ON liability_quote_responses(quote_request_id);
591 CREATE INDEX IF NOT EXISTS idx_liability_quote_responses_provider
592 ON liability_quote_responses(provider_id);
593
594 CREATE TABLE IF NOT EXISTS liability_pricing_authorities (
595 authority_id TEXT PRIMARY KEY,
596 issued_at INTEGER NOT NULL,
597 quote_request_id TEXT NOT NULL REFERENCES liability_quote_requests(quote_request_id),
598 provider_id TEXT NOT NULL,
599 facility_id TEXT NOT NULL,
600 underwriting_decision_id TEXT NOT NULL,
601 expires_at INTEGER NOT NULL,
602 raw_json TEXT NOT NULL,
603 signer_key TEXT NOT NULL,
604 signature TEXT NOT NULL
605 );
606 CREATE UNIQUE INDEX IF NOT EXISTS idx_liability_pricing_authorities_request
607 ON liability_pricing_authorities(quote_request_id);
608 CREATE INDEX IF NOT EXISTS idx_liability_pricing_authorities_provider
609 ON liability_pricing_authorities(provider_id);
610 CREATE INDEX IF NOT EXISTS idx_liability_pricing_authorities_facility
611 ON liability_pricing_authorities(facility_id);
612
613 CREATE TABLE IF NOT EXISTS liability_placements (
614 placement_id TEXT PRIMARY KEY,
615 issued_at INTEGER NOT NULL,
616 quote_request_id TEXT NOT NULL REFERENCES liability_quote_requests(quote_request_id),
617 quote_response_id TEXT NOT NULL REFERENCES liability_quote_responses(quote_response_id),
618 provider_id TEXT NOT NULL,
619 raw_json TEXT NOT NULL,
620 signer_key TEXT NOT NULL,
621 signature TEXT NOT NULL
622 );
623 CREATE UNIQUE INDEX IF NOT EXISTS idx_liability_placements_request
624 ON liability_placements(quote_request_id);
625 CREATE UNIQUE INDEX IF NOT EXISTS idx_liability_placements_response
626 ON liability_placements(quote_response_id);
627 CREATE INDEX IF NOT EXISTS idx_liability_placements_provider
628 ON liability_placements(provider_id);
629
630 CREATE TABLE IF NOT EXISTS liability_bound_coverages (
631 bound_coverage_id TEXT PRIMARY KEY,
632 issued_at INTEGER NOT NULL,
633 quote_request_id TEXT NOT NULL REFERENCES liability_quote_requests(quote_request_id),
634 quote_response_id TEXT NOT NULL REFERENCES liability_quote_responses(quote_response_id),
635 placement_id TEXT NOT NULL REFERENCES liability_placements(placement_id),
636 provider_id TEXT NOT NULL,
637 raw_json TEXT NOT NULL,
638 signer_key TEXT NOT NULL,
639 signature TEXT NOT NULL
640 );
641 CREATE UNIQUE INDEX IF NOT EXISTS idx_liability_bound_coverages_request
642 ON liability_bound_coverages(quote_request_id);
643 CREATE UNIQUE INDEX IF NOT EXISTS idx_liability_bound_coverages_response
644 ON liability_bound_coverages(quote_response_id);
645 CREATE UNIQUE INDEX IF NOT EXISTS idx_liability_bound_coverages_placement
646 ON liability_bound_coverages(placement_id);
647 CREATE INDEX IF NOT EXISTS idx_liability_bound_coverages_provider
648 ON liability_bound_coverages(provider_id);
649
650 CREATE TABLE IF NOT EXISTS liability_auto_bind_decisions (
651 decision_id TEXT PRIMARY KEY,
652 issued_at INTEGER NOT NULL,
653 quote_request_id TEXT NOT NULL REFERENCES liability_quote_requests(quote_request_id),
654 quote_response_id TEXT NOT NULL REFERENCES liability_quote_responses(quote_response_id),
655 authority_id TEXT NOT NULL REFERENCES liability_pricing_authorities(authority_id),
656 provider_id TEXT NOT NULL,
657 disposition TEXT NOT NULL,
658 raw_json TEXT NOT NULL,
659 signer_key TEXT NOT NULL,
660 signature TEXT NOT NULL
661 );
662 CREATE UNIQUE INDEX IF NOT EXISTS idx_liability_auto_bind_decisions_response
663 ON liability_auto_bind_decisions(quote_response_id);
664 CREATE INDEX IF NOT EXISTS idx_liability_auto_bind_decisions_request
665 ON liability_auto_bind_decisions(quote_request_id);
666 CREATE INDEX IF NOT EXISTS idx_liability_auto_bind_decisions_authority
667 ON liability_auto_bind_decisions(authority_id);
668
669 CREATE TABLE IF NOT EXISTS liability_claim_packages (
670 claim_id TEXT PRIMARY KEY,
671 issued_at INTEGER NOT NULL,
672 provider_id TEXT NOT NULL,
673 policy_number TEXT NOT NULL,
674 jurisdiction TEXT NOT NULL,
675 subject_key TEXT NOT NULL,
676 claim_event_at INTEGER NOT NULL,
677 raw_json TEXT NOT NULL,
678 signer_key TEXT NOT NULL,
679 signature TEXT NOT NULL
680 );
681 CREATE INDEX IF NOT EXISTS idx_liability_claim_packages_issued_at
682 ON liability_claim_packages(issued_at);
683 CREATE INDEX IF NOT EXISTS idx_liability_claim_packages_provider
684 ON liability_claim_packages(provider_id);
685 CREATE INDEX IF NOT EXISTS idx_liability_claim_packages_policy_number
686 ON liability_claim_packages(policy_number);
687 CREATE INDEX IF NOT EXISTS idx_liability_claim_packages_subject
688 ON liability_claim_packages(subject_key);
689
690 CREATE TABLE IF NOT EXISTS liability_claim_responses (
691 claim_response_id TEXT PRIMARY KEY,
692 issued_at INTEGER NOT NULL,
693 claim_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_packages(claim_id),
694 provider_id TEXT NOT NULL,
695 disposition TEXT NOT NULL,
696 raw_json TEXT NOT NULL,
697 signer_key TEXT NOT NULL,
698 signature TEXT NOT NULL
699 );
700 CREATE INDEX IF NOT EXISTS idx_liability_claim_responses_issued_at
701 ON liability_claim_responses(issued_at);
702 CREATE INDEX IF NOT EXISTS idx_liability_claim_responses_provider
703 ON liability_claim_responses(provider_id);
704
705 CREATE TABLE IF NOT EXISTS liability_claim_disputes (
706 dispute_id TEXT PRIMARY KEY,
707 issued_at INTEGER NOT NULL,
708 claim_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_packages(claim_id),
709 claim_response_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_responses(claim_response_id),
710 provider_id TEXT NOT NULL,
711 raw_json TEXT NOT NULL,
712 signer_key TEXT NOT NULL,
713 signature TEXT NOT NULL
714 );
715 CREATE INDEX IF NOT EXISTS idx_liability_claim_disputes_issued_at
716 ON liability_claim_disputes(issued_at);
717 CREATE INDEX IF NOT EXISTS idx_liability_claim_disputes_provider
718 ON liability_claim_disputes(provider_id);
719
720 CREATE TABLE IF NOT EXISTS liability_claim_adjudications (
721 adjudication_id TEXT PRIMARY KEY,
722 issued_at INTEGER NOT NULL,
723 claim_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_packages(claim_id),
724 dispute_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_disputes(dispute_id),
725 outcome TEXT NOT NULL,
726 raw_json TEXT NOT NULL,
727 signer_key TEXT NOT NULL,
728 signature TEXT NOT NULL
729 );
730 CREATE INDEX IF NOT EXISTS idx_liability_claim_adjudications_issued_at
731 ON liability_claim_adjudications(issued_at);
732
733 CREATE TABLE IF NOT EXISTS liability_claim_payout_instructions (
734 payout_instruction_id TEXT PRIMARY KEY,
735 issued_at INTEGER NOT NULL,
736 claim_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_packages(claim_id),
737 adjudication_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_adjudications(adjudication_id),
738 payout_amount_units INTEGER NOT NULL,
739 payout_amount_currency TEXT NOT NULL,
740 raw_json TEXT NOT NULL,
741 signer_key TEXT NOT NULL,
742 signature TEXT NOT NULL
743 );
744 CREATE INDEX IF NOT EXISTS idx_liability_claim_payout_instructions_issued_at
745 ON liability_claim_payout_instructions(issued_at);
746
747 CREATE TABLE IF NOT EXISTS liability_claim_payout_receipts (
748 payout_receipt_id TEXT PRIMARY KEY,
749 issued_at INTEGER NOT NULL,
750 claim_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_packages(claim_id),
751 payout_instruction_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_payout_instructions(payout_instruction_id),
752 reconciliation_state TEXT NOT NULL,
753 raw_json TEXT NOT NULL,
754 signer_key TEXT NOT NULL,
755 signature TEXT NOT NULL
756 );
757 CREATE INDEX IF NOT EXISTS idx_liability_claim_payout_receipts_issued_at
758 ON liability_claim_payout_receipts(issued_at);
759
760 CREATE TABLE IF NOT EXISTS liability_claim_settlement_instructions (
761 settlement_instruction_id TEXT PRIMARY KEY,
762 issued_at INTEGER NOT NULL,
763 claim_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_packages(claim_id),
764 payout_receipt_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_payout_receipts(payout_receipt_id),
765 settlement_kind TEXT NOT NULL,
766 payer_role TEXT NOT NULL,
767 payer_id TEXT NOT NULL,
768 payee_role TEXT NOT NULL,
769 payee_id TEXT NOT NULL,
770 settlement_amount_units INTEGER NOT NULL,
771 settlement_amount_currency TEXT NOT NULL,
772 raw_json TEXT NOT NULL,
773 signer_key TEXT NOT NULL,
774 signature TEXT NOT NULL
775 );
776 CREATE INDEX IF NOT EXISTS idx_liability_claim_settlement_instructions_issued_at
777 ON liability_claim_settlement_instructions(issued_at);
778
779 CREATE TABLE IF NOT EXISTS liability_claim_settlement_receipts (
780 settlement_receipt_id TEXT PRIMARY KEY,
781 issued_at INTEGER NOT NULL,
782 claim_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_packages(claim_id),
783 settlement_instruction_id TEXT NOT NULL UNIQUE REFERENCES liability_claim_settlement_instructions(settlement_instruction_id),
784 reconciliation_state TEXT NOT NULL,
785 raw_json TEXT NOT NULL,
786 signer_key TEXT NOT NULL,
787 signature TEXT NOT NULL
788 );
789 CREATE INDEX IF NOT EXISTS idx_liability_claim_settlement_receipts_issued_at
790 ON liability_claim_settlement_receipts(issued_at);
791
792 CREATE TABLE IF NOT EXISTS credit_loss_lifecycle (
793 event_id TEXT PRIMARY KEY,
794 issued_at INTEGER NOT NULL,
795 bond_id TEXT NOT NULL REFERENCES credit_bonds(bond_id),
796 facility_id TEXT,
797 capability_id TEXT,
798 subject_key TEXT,
799 tool_server TEXT,
800 tool_name TEXT,
801 event_kind TEXT NOT NULL,
802 projected_bond_lifecycle_state TEXT NOT NULL,
803 raw_json TEXT NOT NULL,
804 signer_key TEXT NOT NULL,
805 signature TEXT NOT NULL
806 );
807 CREATE INDEX IF NOT EXISTS idx_credit_loss_lifecycle_issued_at
808 ON credit_loss_lifecycle(issued_at);
809 CREATE INDEX IF NOT EXISTS idx_credit_loss_lifecycle_bond
810 ON credit_loss_lifecycle(bond_id);
811 CREATE INDEX IF NOT EXISTS idx_credit_loss_lifecycle_facility
812 ON credit_loss_lifecycle(facility_id);
813 CREATE INDEX IF NOT EXISTS idx_credit_loss_lifecycle_capability
814 ON credit_loss_lifecycle(capability_id);
815 CREATE INDEX IF NOT EXISTS idx_credit_loss_lifecycle_subject
816 ON credit_loss_lifecycle(subject_key);
817 CREATE INDEX IF NOT EXISTS idx_credit_loss_lifecycle_tool
818 ON credit_loss_lifecycle(tool_server, tool_name);
819 CREATE INDEX IF NOT EXISTS idx_credit_loss_lifecycle_kind
820 ON credit_loss_lifecycle(event_kind);
821
822 CREATE TABLE IF NOT EXISTS chio_child_receipts (
823 seq INTEGER PRIMARY KEY AUTOINCREMENT,
824 receipt_id TEXT NOT NULL UNIQUE,
825 timestamp INTEGER NOT NULL,
826 session_id TEXT NOT NULL,
827 parent_request_id TEXT NOT NULL,
828 request_id TEXT NOT NULL,
829 operation_kind TEXT NOT NULL,
830 terminal_state TEXT NOT NULL,
831 policy_hash TEXT NOT NULL,
832 outcome_hash TEXT NOT NULL,
833 raw_json TEXT NOT NULL
834 );
835
836 CREATE INDEX IF NOT EXISTS idx_chio_child_receipts_timestamp
837 ON chio_child_receipts(timestamp);
838 CREATE INDEX IF NOT EXISTS idx_chio_child_receipts_session
839 ON chio_child_receipts(session_id);
840 CREATE INDEX IF NOT EXISTS idx_chio_child_receipts_parent
841 ON chio_child_receipts(parent_request_id);
842 CREATE INDEX IF NOT EXISTS idx_chio_child_receipts_request
843 ON chio_child_receipts(request_id);
844
845 CREATE TABLE IF NOT EXISTS claim_receipt_log_entries (
846 entry_seq INTEGER PRIMARY KEY AUTOINCREMENT,
847 receipt_id TEXT NOT NULL UNIQUE,
848 receipt_kind TEXT NOT NULL,
849 source_seq INTEGER NOT NULL,
850 timestamp INTEGER NOT NULL,
851 capability_id TEXT,
852 session_id TEXT,
853 parent_request_id TEXT,
854 request_id TEXT,
855 subject_key TEXT,
856 issuer_key TEXT,
857 tool_server TEXT,
858 tool_name TEXT,
859 raw_json TEXT NOT NULL,
860 CHECK (entry_seq > 0),
861 CHECK (source_seq > 0),
862 CHECK (receipt_kind IN ('tool_receipt', 'child_receipt')),
863 CHECK (
864 (receipt_kind = 'tool_receipt'
865 AND capability_id IS NOT NULL
866 AND session_id IS NULL
867 AND parent_request_id IS NULL
868 AND request_id IS NULL
869 AND tool_server IS NOT NULL
870 AND tool_name IS NOT NULL)
871 OR
872 (receipt_kind = 'child_receipt'
873 AND capability_id IS NULL
874 AND session_id IS NOT NULL
875 AND parent_request_id IS NOT NULL
876 AND request_id IS NOT NULL
877 AND tool_server IS NULL
878 AND tool_name IS NULL)
879 )
880 );
881 CREATE UNIQUE INDEX IF NOT EXISTS idx_claim_receipt_log_kind_source
882 ON claim_receipt_log_entries(receipt_kind, source_seq);
883 CREATE INDEX IF NOT EXISTS idx_claim_receipt_log_timestamp
884 ON claim_receipt_log_entries(timestamp, entry_seq);
885 CREATE INDEX IF NOT EXISTS idx_claim_receipt_log_tool
886 ON claim_receipt_log_entries(tool_server, tool_name, timestamp)
887 WHERE receipt_kind = 'tool_receipt';
888 CREATE INDEX IF NOT EXISTS idx_claim_receipt_log_child_request
889 ON claim_receipt_log_entries(session_id, request_id, timestamp)
890 WHERE receipt_kind = 'child_receipt';
891
892 DROP TRIGGER IF EXISTS chio_tool_receipts_project_claim_log_entry;
893 CREATE TRIGGER chio_tool_receipts_project_claim_log_entry
894 AFTER INSERT ON chio_tool_receipts
895 BEGIN
896 INSERT INTO claim_receipt_log_entries (
897 receipt_id,
898 receipt_kind,
899 source_seq,
900 timestamp,
901 capability_id,
902 session_id,
903 parent_request_id,
904 request_id,
905 subject_key,
906 issuer_key,
907 tool_server,
908 tool_name,
909 raw_json
910 ) VALUES (
911 NEW.receipt_id,
912 'tool_receipt',
913 NEW.seq,
914 NEW.timestamp,
915 NEW.capability_id,
916 NULL,
917 NULL,
918 NULL,
919 NEW.subject_key,
920 NEW.issuer_key,
921 NEW.tool_server,
922 NEW.tool_name,
923 NEW.raw_json
924 );
925 END;
926
927 DROP TRIGGER IF EXISTS chio_child_receipts_project_claim_log_entry;
928 CREATE TRIGGER chio_child_receipts_project_claim_log_entry
929 AFTER INSERT ON chio_child_receipts
930 BEGIN
931 INSERT INTO claim_receipt_log_entries (
932 receipt_id,
933 receipt_kind,
934 source_seq,
935 timestamp,
936 capability_id,
937 session_id,
938 parent_request_id,
939 request_id,
940 subject_key,
941 issuer_key,
942 tool_server,
943 tool_name,
944 raw_json
945 ) VALUES (
946 NEW.receipt_id,
947 'child_receipt',
948 NEW.seq,
949 NEW.timestamp,
950 NULL,
951 NEW.session_id,
952 NEW.parent_request_id,
953 NEW.request_id,
954 NULL,
955 NULL,
956 NULL,
957 NULL,
958 NEW.raw_json
959 );
960 END;
961
962 CREATE TABLE IF NOT EXISTS session_anchors (
963 anchor_id TEXT PRIMARY KEY,
964 session_id TEXT NOT NULL,
965 auth_context_fingerprint TEXT NOT NULL,
966 issued_at INTEGER NOT NULL,
967 supersedes_anchor_id TEXT REFERENCES session_anchors(anchor_id),
968 is_current INTEGER NOT NULL DEFAULT 1,
969 source_kind TEXT NOT NULL,
970 json_sha256 TEXT NOT NULL,
971 raw_json TEXT NOT NULL
972 );
973 CREATE INDEX IF NOT EXISTS idx_session_anchors_session
974 ON session_anchors(session_id, issued_at DESC);
975 CREATE INDEX IF NOT EXISTS idx_session_anchors_supersedes
976 ON session_anchors(supersedes_anchor_id);
977 CREATE UNIQUE INDEX IF NOT EXISTS idx_session_anchors_current
978 ON session_anchors(session_id)
979 WHERE is_current = 1;
980
981 CREATE TABLE IF NOT EXISTS request_lineage (
982 session_id TEXT NOT NULL,
983 request_id TEXT NOT NULL,
984 parent_request_id TEXT,
985 session_anchor_id TEXT REFERENCES session_anchors(anchor_id),
986 recorded_at INTEGER NOT NULL,
987 request_fingerprint TEXT,
988 source_kind TEXT NOT NULL,
989 json_sha256 TEXT NOT NULL,
990 raw_json TEXT NOT NULL,
991 PRIMARY KEY (session_id, request_id)
992 );
993 CREATE INDEX IF NOT EXISTS idx_request_lineage_parent
994 ON request_lineage(session_id, parent_request_id);
995 CREATE INDEX IF NOT EXISTS idx_request_lineage_anchor
996 ON request_lineage(session_anchor_id);
997 CREATE UNIQUE INDEX IF NOT EXISTS idx_request_lineage_fingerprint
998 ON request_lineage(session_id, COALESCE(session_anchor_id, ''), request_fingerprint)
999 WHERE request_fingerprint IS NOT NULL;
1000
1001 CREATE TABLE IF NOT EXISTS receipt_lineage_statements (
1002 receipt_id TEXT PRIMARY KEY,
1003 statement_id TEXT,
1004 request_id TEXT,
1005 session_id TEXT,
1006 session_anchor_id TEXT REFERENCES session_anchors(anchor_id),
1007 chain_id TEXT,
1008 parent_request_id TEXT,
1009 parent_receipt_id TEXT,
1010 evidence_class TEXT,
1011 evidence_sources_json TEXT,
1012 verified_session_anchor INTEGER NOT NULL DEFAULT 0,
1013 verified_parent_request INTEGER NOT NULL DEFAULT 0,
1014 verified_parent_receipt INTEGER NOT NULL DEFAULT 0,
1015 replay_protected INTEGER NOT NULL DEFAULT 0,
1016 recorded_at INTEGER NOT NULL,
1017 source_kind TEXT NOT NULL,
1018 json_sha256 TEXT NOT NULL,
1019 raw_json TEXT NOT NULL
1020 );
1021 CREATE INDEX IF NOT EXISTS idx_receipt_lineage_request
1022 ON receipt_lineage_statements(session_id, request_id);
1023 CREATE UNIQUE INDEX IF NOT EXISTS idx_receipt_lineage_statement_id
1024 ON receipt_lineage_statements(statement_id)
1025 WHERE statement_id IS NOT NULL;
1026 CREATE INDEX IF NOT EXISTS idx_receipt_lineage_parent_request
1027 ON receipt_lineage_statements(session_id, parent_request_id);
1028 CREATE INDEX IF NOT EXISTS idx_receipt_lineage_parent_receipt
1029 ON receipt_lineage_statements(parent_receipt_id);
1030 CREATE INDEX IF NOT EXISTS idx_receipt_lineage_anchor
1031 ON receipt_lineage_statements(session_anchor_id);
1032 CREATE UNIQUE INDEX IF NOT EXISTS idx_receipt_lineage_request_anchor
1033 ON receipt_lineage_statements(session_id, COALESCE(session_anchor_id, ''), request_id)
1034 WHERE session_id IS NOT NULL
1035 AND request_id IS NOT NULL;
1036
1037 CREATE TABLE IF NOT EXISTS kernel_checkpoints (
1038 id INTEGER PRIMARY KEY AUTOINCREMENT,
1039 checkpoint_seq INTEGER NOT NULL UNIQUE,
1040 batch_start_seq INTEGER NOT NULL,
1041 batch_end_seq INTEGER NOT NULL,
1042 tree_size INTEGER NOT NULL,
1043 merkle_root TEXT NOT NULL,
1044 issued_at INTEGER NOT NULL,
1045 statement_json TEXT NOT NULL,
1046 signature TEXT NOT NULL,
1047 kernel_key TEXT NOT NULL
1048 );
1049 CREATE INDEX IF NOT EXISTS idx_kernel_checkpoints_batch_end
1050 ON kernel_checkpoints(batch_end_seq);
1051
1052 CREATE TABLE IF NOT EXISTS checkpoint_tree_heads (
1053 checkpoint_seq INTEGER PRIMARY KEY
1054 REFERENCES kernel_checkpoints(checkpoint_seq) ON DELETE CASCADE,
1055 batch_start_seq INTEGER NOT NULL,
1056 batch_end_seq INTEGER NOT NULL,
1057 tree_size INTEGER NOT NULL,
1058 merkle_root TEXT NOT NULL,
1059 issued_at INTEGER NOT NULL,
1060 kernel_key TEXT NOT NULL,
1061 previous_checkpoint_sha256 TEXT,
1062 statement_json TEXT NOT NULL,
1063 signature TEXT NOT NULL
1064 );
1065 CREATE INDEX IF NOT EXISTS idx_checkpoint_tree_heads_tree_size
1066 ON checkpoint_tree_heads(tree_size);
1067 CREATE INDEX IF NOT EXISTS idx_checkpoint_tree_heads_previous
1068 ON checkpoint_tree_heads(previous_checkpoint_sha256);
1069
1070 CREATE TABLE IF NOT EXISTS checkpoint_predecessor_witnesses (
1071 predecessor_checkpoint_seq INTEGER NOT NULL
1072 REFERENCES checkpoint_tree_heads(checkpoint_seq) ON DELETE CASCADE,
1073 witness_checkpoint_seq INTEGER PRIMARY KEY
1074 REFERENCES checkpoint_tree_heads(checkpoint_seq) ON DELETE CASCADE,
1075 previous_checkpoint_sha256 TEXT NOT NULL,
1076 witnessed_at INTEGER NOT NULL,
1077 witness_statement_json TEXT NOT NULL
1078 );
1079 CREATE INDEX IF NOT EXISTS idx_checkpoint_predecessor_witnesses_predecessor
1080 ON checkpoint_predecessor_witnesses(predecessor_checkpoint_seq);
1081 CREATE INDEX IF NOT EXISTS idx_checkpoint_predecessor_witnesses_previous
1082 ON checkpoint_predecessor_witnesses(previous_checkpoint_sha256);
1083
1084 CREATE TABLE IF NOT EXISTS checkpoint_publication_metadata (
1085 checkpoint_seq INTEGER PRIMARY KEY
1086 REFERENCES kernel_checkpoints(checkpoint_seq) ON DELETE CASCADE,
1087 publication_schema TEXT NOT NULL,
1088 merkle_root TEXT NOT NULL,
1089 published_at INTEGER NOT NULL,
1090 kernel_key TEXT NOT NULL,
1091 log_tree_size INTEGER NOT NULL,
1092 entry_start_seq INTEGER NOT NULL,
1093 entry_end_seq INTEGER NOT NULL,
1094 previous_checkpoint_sha256 TEXT
1095 );
1096 CREATE INDEX IF NOT EXISTS idx_checkpoint_publication_metadata_published_at
1097 ON checkpoint_publication_metadata(published_at);
1098 CREATE INDEX IF NOT EXISTS idx_checkpoint_publication_metadata_log_tree_size
1099 ON checkpoint_publication_metadata(log_tree_size);
1100 CREATE INDEX IF NOT EXISTS idx_checkpoint_publication_metadata_previous
1101 ON checkpoint_publication_metadata(previous_checkpoint_sha256);
1102
1103 CREATE TABLE IF NOT EXISTS checkpoint_publication_trust_anchor_bindings (
1104 checkpoint_seq INTEGER PRIMARY KEY
1105 REFERENCES kernel_checkpoints(checkpoint_seq) ON DELETE CASCADE,
1106 binding_json TEXT NOT NULL
1107 );
1108
1109 DROP TRIGGER IF EXISTS kernel_checkpoints_project_tree_head;
1110 CREATE TRIGGER kernel_checkpoints_project_tree_head
1111 AFTER INSERT ON kernel_checkpoints
1112 BEGIN
1113 INSERT INTO checkpoint_tree_heads (
1114 checkpoint_seq,
1115 batch_start_seq,
1116 batch_end_seq,
1117 tree_size,
1118 merkle_root,
1119 issued_at,
1120 kernel_key,
1121 previous_checkpoint_sha256,
1122 statement_json,
1123 signature
1124 ) VALUES (
1125 NEW.checkpoint_seq,
1126 NEW.batch_start_seq,
1127 NEW.batch_end_seq,
1128 NEW.tree_size,
1129 NEW.merkle_root,
1130 NEW.issued_at,
1131 NEW.kernel_key,
1132 CAST(json_extract(NEW.statement_json, '$.previous_checkpoint_sha256') AS TEXT),
1133 NEW.statement_json,
1134 NEW.signature
1135 );
1136
1137 INSERT INTO checkpoint_predecessor_witnesses (
1138 predecessor_checkpoint_seq,
1139 witness_checkpoint_seq,
1140 previous_checkpoint_sha256,
1141 witnessed_at,
1142 witness_statement_json
1143 )
1144 SELECT
1145 NEW.checkpoint_seq - 1,
1146 NEW.checkpoint_seq,
1147 CAST(json_extract(NEW.statement_json, '$.previous_checkpoint_sha256') AS TEXT),
1148 NEW.issued_at,
1149 NEW.statement_json
1150 WHERE json_extract(NEW.statement_json, '$.previous_checkpoint_sha256') IS NOT NULL;
1151
1152 INSERT INTO checkpoint_publication_metadata (
1153 checkpoint_seq,
1154 publication_schema,
1155 merkle_root,
1156 published_at,
1157 kernel_key,
1158 log_tree_size,
1159 entry_start_seq,
1160 entry_end_seq,
1161 previous_checkpoint_sha256
1162 ) VALUES (
1163 NEW.checkpoint_seq,
1164 'chio.checkpoint_publication.v1',
1165 NEW.merkle_root,
1166 NEW.issued_at,
1167 NEW.kernel_key,
1168 NEW.batch_end_seq,
1169 NEW.batch_start_seq,
1170 NEW.batch_end_seq,
1171 CAST(json_extract(NEW.statement_json, '$.previous_checkpoint_sha256') AS TEXT)
1172 );
1173 END;
1174
1175 CREATE TABLE IF NOT EXISTS capability_lineage (
1176 capability_id TEXT PRIMARY KEY,
1177 subject_key TEXT NOT NULL,
1178 issuer_key TEXT NOT NULL,
1179 issued_at INTEGER NOT NULL,
1180 expires_at INTEGER NOT NULL,
1181 grants_json TEXT NOT NULL,
1182 delegation_depth INTEGER NOT NULL DEFAULT 0,
1183 parent_capability_id TEXT REFERENCES capability_lineage(capability_id),
1184 federated_parent_capability_id TEXT,
1185 provenance TEXT NOT NULL DEFAULT 'legacy_projection'
1186 CHECK (provenance IN ('signed_token', 'synthetic_anchor', 'legacy_projection')),
1187 signed_capability_json TEXT
1188 );
1189 CREATE INDEX IF NOT EXISTS idx_capability_lineage_subject
1190 ON capability_lineage(subject_key);
1191 CREATE INDEX IF NOT EXISTS idx_capability_lineage_issuer
1192 ON capability_lineage(issuer_key);
1193 CREATE INDEX IF NOT EXISTS idx_capability_lineage_issued_at
1194 ON capability_lineage(issued_at);
1195 CREATE INDEX IF NOT EXISTS idx_capability_lineage_parent
1196 ON capability_lineage(parent_capability_id);
1197 CREATE TABLE IF NOT EXISTS federated_lineage_bridges (
1198 local_capability_id TEXT PRIMARY KEY REFERENCES capability_lineage(capability_id) ON DELETE CASCADE,
1199 parent_capability_id TEXT NOT NULL,
1200 share_id TEXT REFERENCES federated_evidence_shares(share_id)
1201 );
1202 CREATE INDEX IF NOT EXISTS idx_federated_lineage_bridges_parent
1203 ON federated_lineage_bridges(parent_capability_id);
1204
1205 CREATE TABLE IF NOT EXISTS federated_evidence_shares (
1206 share_id TEXT PRIMARY KEY,
1207 manifest_hash TEXT NOT NULL,
1208 imported_at INTEGER NOT NULL,
1209 exported_at INTEGER NOT NULL,
1210 issuer TEXT NOT NULL,
1211 partner TEXT NOT NULL,
1212 signer_public_key TEXT NOT NULL,
1213 require_proofs INTEGER NOT NULL DEFAULT 0,
1214 query_json TEXT NOT NULL
1215 );
1216 CREATE INDEX IF NOT EXISTS idx_federated_evidence_shares_imported_at
1217 ON federated_evidence_shares(imported_at);
1218
1219 CREATE TABLE IF NOT EXISTS federated_share_tool_receipts (
1220 share_id TEXT NOT NULL REFERENCES federated_evidence_shares(share_id) ON DELETE CASCADE,
1221 seq INTEGER NOT NULL,
1222 receipt_id TEXT NOT NULL,
1223 timestamp INTEGER NOT NULL,
1224 capability_id TEXT NOT NULL,
1225 subject_key TEXT,
1226 issuer_key TEXT,
1227 raw_json TEXT NOT NULL,
1228 PRIMARY KEY (share_id, seq),
1229 UNIQUE (share_id, receipt_id)
1230 );
1231 CREATE INDEX IF NOT EXISTS idx_federated_share_receipts_capability
1232 ON federated_share_tool_receipts(capability_id);
1233 CREATE INDEX IF NOT EXISTS idx_federated_share_receipts_subject
1234 ON federated_share_tool_receipts(subject_key);
1235
1236 CREATE TABLE IF NOT EXISTS federated_share_capability_lineage (
1237 share_id TEXT NOT NULL REFERENCES federated_evidence_shares(share_id) ON DELETE CASCADE,
1238 capability_id TEXT NOT NULL,
1239 subject_key TEXT NOT NULL,
1240 issuer_key TEXT NOT NULL,
1241 issued_at INTEGER NOT NULL,
1242 expires_at INTEGER NOT NULL,
1243 grants_json TEXT NOT NULL,
1244 delegation_depth INTEGER NOT NULL DEFAULT 0,
1245 parent_capability_id TEXT,
1246 federated_parent_capability_id TEXT,
1247 provenance TEXT NOT NULL DEFAULT 'legacy_projection'
1248 CHECK (provenance IN ('signed_token', 'synthetic_anchor', 'legacy_projection')),
1249 signed_capability_json TEXT,
1250 PRIMARY KEY (share_id, capability_id)
1251 );
1252 CREATE INDEX IF NOT EXISTS idx_federated_share_lineage_capability
1253 ON federated_share_capability_lineage(capability_id);
1254 CREATE INDEX IF NOT EXISTS idx_federated_share_lineage_subject
1255 ON federated_share_capability_lineage(subject_key);
1256
1257 CREATE TABLE IF NOT EXISTS receipt_retention_watermark (
1258 archived_through_entry_seq INTEGER NOT NULL,
1259 archived_through_timestamp INTEGER NOT NULL,
1260 archive_path TEXT NOT NULL,
1261 archive_sha256 TEXT,
1262 rotated_at INTEGER NOT NULL,
1263 CHECK (archived_through_entry_seq >= 0)
1264 );
1265
1266 "#,
1267 )?;
1268 schema_migration.commit()?;
1269 connection.execute_batch(crate::IOU_ENVELOPE_MIGRATION)?;
1270 connection.execute_batch(crate::dead_letters::SETTLE_DEAD_LETTERS_MIGRATION)?;
1271 connection.execute_batch(crate::settle_attempts::SETTLE_ATTEMPTS_MIGRATION)?;
1272 ensure_tool_receipt_attribution_columns(&connection)?;
1273 super::support::ensure_receipt_lineage_statement_columns(&connection)?;
1274 super::support::drop_transparency_projection_guards(&connection)?;
1275 let backfill_result = (|| -> Result<(), ReceiptStoreError> {
1276 super::support::ensure_receipt_retention_watermark_table(&connection)?;
1277 super::support::ensure_receipt_retention_tombstones(&connection)?;
1278 backfill_tool_receipt_attribution_columns(&connection)?;
1279 super::support::backfill_provenance_lineage_tables(&mut connection)?;
1280 super::support::backfill_claim_receipt_log_entries(&mut connection)?;
1281 super::support::backfill_checkpoint_transparency_projections(&mut connection)?;
1282 if on_disk_schema_version < RECEIPT_COST_PROJECTION_SCHEMA_VERSION {
1283 let migration = connection
1284 .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
1285 migrate_receipt_cost_projection(&migration)?;
1286 migration.commit()?;
1287 }
1288 Ok(())
1289 })();
1290 let guard_result = super::support::ensure_transparency_projection_guards(&connection);
1291 match (backfill_result, guard_result) {
1292 (Ok(()), Ok(())) => {}
1293 (Err(error), Ok(())) => return Err(error),
1294 (Ok(()), Err(error)) => return Err(error),
1295 (Err(backfill_error), Err(guard_error)) => {
1296 return Err(ReceiptStoreError::Canonical(format!(
1297 "receipt projection backfill failed ({backfill_error}); restoring immutability guards also failed ({guard_error})"
1298 )));
1299 }
1300 }
1301 verify_receipt_cost_projection(&connection)?;
1302
1303 let migration =
1304 connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
1305 ensure_capability_lineage_provenance_columns(&migration)?;
1306 crate::stamp_schema_version(
1307 &migration,
1308 RECEIPT_STORE_SCHEMA_KEY,
1309 RECEIPT_STORE_SUPPORTED_SCHEMA_VERSION,
1310 )
1311 .map_err(|error| ReceiptStoreError::Conflict(error.to_string()))?;
1312 migration.commit()?;
1313
1314 let settlement_store_binding = settlement_store_binding_if_ready(&connection)?;
1315
1316 drop(connection);
1317
1318 let reader_pool = build_receipt_pool(
1319 path,
1320 options.pool.reader_pool_max_size,
1321 "reader",
1322 connection_flags,
1323 options.pool.max_page_count,
1324 )?;
1325 let writer_pool = build_receipt_pool(
1326 path,
1327 options.pool.writer_pool_max_size,
1328 "writer",
1329 connection_flags,
1330 options.pool.max_page_count,
1331 )?;
1332
1333 Ok(Self {
1334 receipt_commit_actor: ReceiptCommitActor::start(
1335 writer_pool,
1336 options.incremental_verification,
1337 ),
1338 pool: reader_pool,
1339 settlement_store_binding,
1340 strict_tenant_isolation: std::sync::atomic::AtomicBool::new(true),
1341 incremental_verification: options.incremental_verification,
1342 })
1343 }
1344
1345 pub fn open_with_pool_sizes(
1346 path: impl AsRef<Path>,
1347 reader_pool_max_size: u32,
1348 writer_pool_max_size: u32,
1349 ) -> Result<Self, ReceiptStoreError> {
1350 Self::open_with_pool_config(
1351 path,
1352 crate::SqlitePoolConfig {
1353 reader_pool_max_size,
1354 writer_pool_max_size,
1355 max_page_count: None,
1356 },
1357 )
1358 }
1359}
1360
1361fn build_receipt_pool(
1362 path: &Path,
1363 max_size: u32,
1364 pool_name: &str,
1365 flags: Option<rusqlite::OpenFlags>,
1366 max_page_count: Option<u32>,
1367) -> Result<Pool<SqliteConnectionManager>, ReceiptStoreError> {
1368 if max_size == 0 {
1369 return Err(ReceiptStoreError::Pool(format!(
1370 "{pool_name} receipt sqlite pool max_size must be greater than zero"
1371 )));
1372 }
1373 let mut manager = SqliteConnectionManager::file(path);
1374 if let Some(flags) = flags {
1375 manager = manager.with_flags(flags);
1376 }
1377 let manager = manager.with_init(move |connection| {
1378 configure_sqlite_connection(connection, max_page_count).map_err(|error| match error {
1379 ReceiptStoreError::Sqlite(error) => error,
1380 other => rusqlite::Error::InvalidParameterName(other.to_string()),
1381 })
1382 });
1383 Pool::builder()
1384 .max_size(max_size)
1385 .build(manager)
1386 .map_err(|error| ReceiptStoreError::Pool(error.to_string()))
1387}
1388
1389fn existing_database_open_flags() -> rusqlite::OpenFlags {
1390 rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX
1391}
1392
1393fn require_existing_receipt_schema(
1394 path: &Path,
1395 connection: &Connection,
1396) -> Result<(), ReceiptStoreError> {
1397 const REQUIRED_TABLES: &[&str] = &[
1398 "chio_tool_receipts",
1399 "chio_child_receipts",
1400 "claim_receipt_log_entries",
1401 "kernel_checkpoints",
1402 "checkpoint_tree_heads",
1403 "checkpoint_predecessor_witnesses",
1404 "checkpoint_publication_metadata",
1405 ];
1406
1407 for table in REQUIRED_TABLES {
1408 let exists: Option<String> = connection
1409 .query_row(
1410 "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?1",
1411 [*table],
1412 |row| row.get(0),
1413 )
1414 .optional()?;
1415 if exists.is_none() {
1416 return Err(ReceiptStoreError::NotFound(format!(
1417 "receipt database {} is not an initialized Chio receipt store: missing table {table}",
1418 path.display()
1419 )));
1420 }
1421 }
1422
1423 Ok(())
1424}
1425
1426fn reject_foreign_legacy_receipt_anchor(connection: &Connection) -> Result<(), ReceiptStoreError> {
1433 let application_id: i32 =
1434 connection.query_row("PRAGMA application_id", [], |row| row.get(0))?;
1435 if application_id != 0 {
1436 return Ok(());
1437 }
1438 for anchor in RECEIPT_STORE_GENERIC_LEGACY_ANCHOR_TABLES {
1439 if table_exists(connection, anchor)?
1440 && !table_has_receipt_payload_column(connection, anchor)?
1441 {
1442 return Err(ReceiptStoreError::Conflict(format!(
1443 "database has a `{anchor}` table without a receipt payload column \
1444 (raw_json or receipt_json); refusing to adopt a foreign database as a receipt store"
1445 )));
1446 }
1447 }
1448 Ok(())
1449}
1450
1451fn table_exists(connection: &Connection, table: &str) -> Result<bool, ReceiptStoreError> {
1452 let present: bool = connection.query_row(
1453 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1)",
1454 [table],
1455 |row| row.get(0),
1456 )?;
1457 Ok(present)
1458}
1459
1460fn table_has_receipt_payload_column(
1465 connection: &Connection,
1466 table: &str,
1467) -> Result<bool, ReceiptStoreError> {
1468 let mut statement = connection.prepare(&format!("PRAGMA table_info({table})"))?;
1469 let mut rows = statement.query([])?;
1470 while let Some(row) = rows.next()? {
1471 let column: String = row.get(1)?;
1472 if column.eq_ignore_ascii_case("raw_json") || column.eq_ignore_ascii_case("receipt_json") {
1473 return Ok(true);
1474 }
1475 }
1476 Ok(false)
1477}
1478
1479fn ensure_capability_lineage_provenance_columns(
1480 connection: &Connection,
1481) -> Result<(), ReceiptStoreError> {
1482 for table in ["capability_lineage", "federated_share_capability_lineage"] {
1483 if !table_has_column(connection, table, "signed_capability_json")? {
1484 connection.execute(
1485 &format!("ALTER TABLE {table} ADD COLUMN signed_capability_json TEXT"),
1486 [],
1487 )?;
1488 }
1489 if !table_has_column(connection, table, "federated_parent_capability_id")? {
1490 connection.execute(
1491 &format!("ALTER TABLE {table} ADD COLUMN federated_parent_capability_id TEXT"),
1492 [],
1493 )?;
1494 }
1495 if !table_has_column(connection, table, "provenance")? {
1496 connection.execute(
1497 &format!(
1498 "ALTER TABLE {table} ADD COLUMN provenance TEXT NOT NULL DEFAULT \
1499 'legacy_projection' CHECK (provenance IN ('signed_token', \
1500 'synthetic_anchor', 'legacy_projection'))"
1501 ),
1502 [],
1503 )?;
1504 }
1505 connection.execute(
1506 &format!(
1507 "UPDATE {table} SET provenance = 'signed_token' \
1508 WHERE provenance = 'legacy_projection' \
1509 AND signed_capability_json IS NOT NULL"
1510 ),
1511 [],
1512 )?;
1513 }
1514
1515 let conflicting_bridge: bool = connection.query_row(
1516 r#"
1517 SELECT EXISTS(
1518 SELECT 1
1519 FROM capability_lineage c
1520 INNER JOIN federated_lineage_bridges b
1521 ON b.local_capability_id = c.capability_id
1522 WHERE c.federated_parent_capability_id IS NOT NULL
1523 AND c.federated_parent_capability_id != b.parent_capability_id
1524 )
1525 "#,
1526 [],
1527 |row| row.get(0),
1528 )?;
1529 if conflicting_bridge {
1530 return Err(ReceiptStoreError::Conflict(
1531 "capability lineage federation metadata conflicts with its persisted bridge"
1532 .to_string(),
1533 ));
1534 }
1535 let bridges_to_backfill = {
1536 let mut statement = connection.prepare(
1537 r#"
1538 SELECT b.local_capability_id, b.parent_capability_id
1539 FROM federated_lineage_bridges b
1540 INNER JOIN capability_lineage c
1541 ON c.capability_id = b.local_capability_id
1542 WHERE c.federated_parent_capability_id IS NULL
1543 ORDER BY c.rowid
1544 "#,
1545 )?;
1546 let bridges = statement
1547 .query_map([], |row| {
1548 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1549 })?
1550 .collect::<Result<Vec<_>, _>>()?;
1551 bridges
1552 };
1553 let mut next_rowid: i64 = connection.query_row(
1554 "SELECT COALESCE(MAX(rowid), 0) FROM capability_lineage",
1555 [],
1556 |row| row.get(0),
1557 )?;
1558 for (local_capability_id, parent_capability_id) in bridges_to_backfill {
1559 next_rowid = next_rowid.checked_add(1).ok_or_else(|| {
1560 ReceiptStoreError::Conflict(
1561 "capability lineage replication sequence is exhausted".to_string(),
1562 )
1563 })?;
1564 connection.execute(
1565 r#"
1566 UPDATE capability_lineage
1567 SET federated_parent_capability_id = ?2,
1568 rowid = ?3
1569 WHERE capability_id = ?1
1570 AND federated_parent_capability_id IS NULL
1571 "#,
1572 params![local_capability_id, parent_capability_id, next_rowid],
1573 )?;
1574 }
1575 connection.execute(
1576 "CREATE INDEX IF NOT EXISTS idx_capability_lineage_federated_parent \
1577 ON capability_lineage(federated_parent_capability_id)",
1578 [],
1579 )?;
1580 Ok(())
1581}
1582
1583fn table_has_column(
1584 connection: &Connection,
1585 table: &str,
1586 expected_column: &str,
1587) -> Result<bool, ReceiptStoreError> {
1588 let mut statement = connection.prepare(&format!("PRAGMA table_info({table})"))?;
1589 let mut rows = statement.query([])?;
1590 while let Some(row) = rows.next()? {
1591 let column: String = row.get(1)?;
1592 if column.eq_ignore_ascii_case(expected_column) {
1593 return Ok(true);
1594 }
1595 }
1596 Ok(false)
1597}