use super::support::{
ensure_checkpoint_transparency_guards, ensure_transparency_projection_guards,
insert_receipt_retention_watermark, load_claim_tree_canonical_bytes_range,
load_persisted_checkpoint_row, parse_persisted_checkpoint_row, store_kernel_checkpoint_atomic,
};
use super::*;
pub(crate) fn receipt_query_sql(
query: &ReceiptQuery,
tenant_fragment: &str,
) -> Result<(String, String), ReceiptStoreError> {
let currency = query
.validated_cost_currency()
.map_err(ReceiptStoreError::ReadBoundary)?;
let cost_fragment = match (
query.min_cost.is_some(),
query.max_cost.is_some(),
currency.is_some(),
) {
(false, false, false) => "AND ?13 IS NULL",
(false, false, true) => "AND r.cost_currency = ?13",
(true, false, true) => "AND r.cost_currency = ?13 AND r.cost_charged_be >= ?7",
(false, true, true) => "AND r.cost_currency = ?13 AND r.cost_charged_be <= ?8",
(true, true, true) => {
"AND r.cost_currency = ?13 AND r.cost_charged_be >= ?7 AND r.cost_charged_be <= ?8"
}
_ => {
return Err(ReceiptStoreError::ReadBoundary(
"receipt query cost bounds require a currency".to_string(),
))
}
};
let from_where = format!(
r#"
FROM chio_tool_receipts r
LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
WHERE (?1 IS NULL OR r.capability_id = ?1)
AND (?2 IS NULL OR r.tool_server = ?2)
AND (?3 IS NULL OR r.tool_name = ?3)
AND (?4 IS NULL OR r.decision_kind = ?4)
AND (?5 IS NULL OR r.timestamp >= ?5)
AND (?6 IS NULL OR r.timestamp <= ?6)
{cost_fragment}
AND (?9 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?9)
AND {tenant_fragment}
"#
);
let data_sql = format!(
r#"
SELECT r.seq, r.raw_json
{from_where}
AND (?10 IS NULL OR r.seq > ?10)
ORDER BY r.seq ASC
LIMIT ?11
"#
);
let count_sql = format!("SELECT COUNT(*) {from_where}");
Ok((data_sql, count_sql))
}
impl SqliteReceiptStore {
pub fn append_chio_receipt_returning_seq(
&self,
receipt: &ChioReceipt,
) -> Result<u64, ReceiptStoreError> {
let raw_json = serde_json::to_string(receipt)?;
self.append_verified_chio_receipt_record(receipt, &raw_json, false)
}
pub fn store_checkpoint(&self, checkpoint: &KernelCheckpoint) -> Result<(), ReceiptStoreError> {
let checkpoint = checkpoint.clone();
self.writer_handle()
.run_write(move |connection| store_kernel_checkpoint_atomic(connection, &checkpoint))
}
pub fn load_checkpoint_by_seq(
&self,
checkpoint_seq: u64,
) -> Result<Option<KernelCheckpoint>, ReceiptStoreError> {
let connection = self.connection()?;
ensure_checkpoint_transparency_guards(&connection)?;
load_persisted_checkpoint_row(&connection, checkpoint_seq)?
.map(parse_persisted_checkpoint_row)
.transpose()
}
pub fn receipts_canonical_bytes_range(
&self,
start_seq: u64,
end_seq: u64,
) -> Result<Vec<(u64, Vec<u8>)>, ReceiptStoreError> {
let connection = self.connection()?;
load_claim_tree_canonical_bytes_range(&connection, start_seq, end_seq)
}
pub fn db_size_bytes(&self) -> Result<u64, ReceiptStoreError> {
let page_count: i64 = self
.connection()?
.query_row("PRAGMA page_count", [], |row| row.get(0))?;
let page_size: i64 = self
.connection()?
.query_row("PRAGMA page_size", [], |row| row.get(0))?;
Ok((page_count.max(0) as u64) * (page_size.max(0) as u64))
}
pub fn live_db_size_bytes(&self) -> Result<u64, ReceiptStoreError> {
let connection = self.connection()?;
live_db_size_bytes_on_connection(&connection)
}
pub fn oldest_receipt_timestamp(&self) -> Result<Option<u64>, ReceiptStoreError> {
let ts = self.connection()?.query_row(
"SELECT MIN(timestamp) FROM chio_tool_receipts",
[],
|row| row.get::<_, Option<i64>>(0),
)?;
Ok(ts.map(|t| t.max(0) as u64))
}
pub fn oldest_receipt_timestamp_for_tenant(
&self,
tenant_id: &str,
) -> Result<Option<u64>, ReceiptStoreError> {
let ts = self.connection()?.query_row(
"SELECT MIN(timestamp) FROM chio_tool_receipts WHERE tenant_id = ?1",
params![tenant_id],
|row| row.get::<_, Option<i64>>(0),
)?;
Ok(ts.map(|t| t.max(0) as u64))
}
pub fn archive_receipts_before(
&self,
cutoff_unix_secs: u64,
archive_path: &str,
) -> Result<u64, ReceiptStoreError> {
let config = RetentionConfig {
retention_days: 0,
max_size_bytes: u64::MAX,
archive_path: archive_path.to_string(),
tenant_id: None,
explicit_cutoff_unix_secs: Some(cutoff_unix_secs),
..RetentionConfig::default()
};
self.dispatch_rotate(Box::new(config), Some(cutoff_unix_secs))
}
pub fn rotate_if_needed(&self, config: &RetentionConfig) -> Result<u64, ReceiptStoreError> {
self.dispatch_rotate(Box::new(config.clone()), None)
}
fn dispatch_rotate(
&self,
config: Box<RetentionConfig>,
explicit_cutoff: Option<u64>,
) -> Result<u64, ReceiptStoreError> {
if config.tenant_id.is_some() {
return Err(ReceiptStoreError::RetentionTenantScopeUnsupported);
}
let config = match explicit_cutoff {
Some(cutoff) => {
let mut config = config;
config.retention_days = 0;
config.explicit_cutoff_unix_secs = Some(cutoff);
config
}
None => config,
};
let (response, result) = std::sync::mpsc::sync_channel(1);
let health = &self.receipt_commit_actor.health;
health
.inflight
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if let Err(error) = self
.receipt_commit_actor
.sender
.try_send(ReceiptCommitCommand::Rotate { config, response })
{
atomic_saturating_sub(&health.inflight, 1);
return Err(match error {
std::sync::mpsc::TrySendError::Full(_) => receipt_actor_saturated_error(),
std::sync::mpsc::TrySendError::Disconnected(_) => receipt_actor_unavailable_error(),
});
}
match result.recv() {
Ok(outcome) => outcome,
Err(_) => {
atomic_saturating_sub(&health.inflight, 1);
Err(receipt_actor_unavailable_error())
}
}
}
pub(crate) fn query_receipts_impl(
&self,
query: &ReceiptQuery,
) -> Result<ReceiptQueryResult, ReceiptStoreError> {
const VALID_OUTCOMES: &[&str] = &["allow", "deny", "cancelled", "incomplete"];
if let Some(outcome) = query.outcome.as_deref() {
if !VALID_OUTCOMES.contains(&outcome) {
return Err(ReceiptStoreError::InvalidOutcome(format!(
"unknown outcome filter {:?}; valid values are: allow, deny, cancelled, incomplete",
outcome
)));
}
}
let limit = query.limit.clamp(1, MAX_QUERY_LIMIT);
let read_scope = query
.effective_read_scope()
.map_err(ReceiptStoreError::ReadBoundary)?;
let tenant_fragment = match (
read_scope.tenant.as_deref(),
read_scope.include_null_tenant && !self.strict_tenant_isolation_enabled(),
) {
(None, _) => "(?12 IS NULL)",
(Some(_), true) => "(r.tenant_id = ?12 OR r.tenant_id IS NULL)",
(Some(_), false) => "(r.tenant_id = ?12)",
};
let (data_sql, count_sql) = receipt_query_sql(query, tenant_fragment)?;
let cap_id = query.capability_id.as_deref();
let tool_srv = query.tool_server.as_deref();
let tool_nm = query.tool_name.as_deref();
let outcome = query.outcome.as_deref();
let since = query.since.map(|v| v as i64);
let until = query.until.map(|v| v as i64);
let min_cost = query.min_cost.map(|value| value.to_be_bytes().to_vec());
let max_cost = query.max_cost.map(|value| value.to_be_bytes().to_vec());
let agent_sub = query.agent_subject.as_deref();
let tenant = read_scope.tenant.as_deref();
let cost_currency = query.cost_currency.as_deref();
let mut connection = self.connection()?;
let transaction =
connection.transaction_with_behavior(rusqlite::TransactionBehavior::Deferred)?;
let cursor_i64: Option<i64> = match query.cursor {
None => None,
Some(c) => match i64::try_from(c) {
Ok(v) => Some(v),
Err(_) => {
let total_count: u64 = transaction
.query_row(
&count_sql,
params![
cap_id,
tool_srv,
tool_nm,
outcome,
since,
until,
min_cost,
max_cost,
agent_sub,
None::<i64>,
0i64,
tenant,
cost_currency,
],
|row| row.get::<_, i64>(0),
)
.map(|n| n.max(0) as u64)?;
transaction.commit()?;
return Ok(ReceiptQueryResult {
receipts: Vec::new(),
total_count,
next_cursor: None,
});
}
},
};
let receipts = {
let mut statement = transaction.prepare(&data_sql)?;
let rows = statement.query_map(
params![
cap_id,
tool_srv,
tool_nm,
outcome,
since,
until,
min_cost,
max_cost,
agent_sub,
cursor_i64,
limit as i64,
tenant,
cost_currency,
],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
)?;
let mut receipts = Vec::new();
for row in rows {
let (seq, raw_json) = row?;
let seq = seq.max(0) as u64;
let receipt =
decode_verified_chio_receipt(&raw_json, "persisted tool receipt", Some(seq))?;
receipts.push(StoredToolReceipt { seq, receipt });
}
receipts
};
let total_count: u64 = transaction
.query_row(
&count_sql,
params![
cap_id,
tool_srv,
tool_nm,
outcome,
since,
until,
min_cost,
max_cost,
agent_sub,
None::<i64>,
0i64,
tenant,
cost_currency,
],
|row| row.get::<_, i64>(0),
)
.map(|n| n.max(0) as u64)?;
let next_cursor = if receipts.len() == limit {
receipts.last().map(|r| r.seq)
} else {
None
};
transaction.commit()?;
Ok(ReceiptQueryResult {
receipts,
total_count,
next_cursor,
})
}
}
fn compute_archival_watermark(
connection: &rusqlite::Connection,
cutoff_unix_secs: u64,
) -> Result<u64, ReceiptStoreError> {
let cutoff = sqlite_i64(cutoff_unix_secs, "retention cutoff")?;
let settlement_attempts_installed: bool = connection.query_row(
"SELECT EXISTS(SELECT 1 FROM main.sqlite_master \
WHERE type = 'table' AND name = 'settle_attempts')",
[],
|row| row.get(0),
)?;
let active_settlement_guard = if settlement_attempts_installed {
r#"
AND NOT EXISTS (
SELECT 1 FROM settle_attempts sa
JOIN claim_receipt_log_entries e ON e.receipt_id = sa.receipt_id
WHERE e.entry_seq <= kc.batch_end_seq
)
"#
} else {
""
};
let query = format!(
r#"
SELECT COALESCE(MAX(kc.batch_end_seq), 0)
FROM kernel_checkpoints kc
WHERE NOT EXISTS (
SELECT 1 FROM claim_receipt_log_entries e
WHERE e.entry_seq <= kc.batch_end_seq
AND e.timestamp >= ?1
)
AND NOT EXISTS (
SELECT 1 FROM chio_authorization_receipt_consumptions ac
JOIN claim_receipt_log_entries ae ON ae.receipt_id = ac.authorization_receipt_id
JOIN claim_receipt_log_entries ce ON ce.receipt_id = ac.consumer_receipt_id
WHERE ae.entry_seq <= kc.batch_end_seq
AND ce.entry_seq > kc.batch_end_seq
)
AND NOT EXISTS (
SELECT 1 FROM receipt_lineage_statements ls
JOIN claim_receipt_log_entries pe ON pe.receipt_id = ls.parent_receipt_id
JOIN claim_receipt_log_entries ce ON ce.receipt_id = ls.receipt_id
WHERE pe.entry_seq <= kc.batch_end_seq
AND ce.entry_seq > kc.batch_end_seq
)
AND NOT EXISTS (
SELECT 1 FROM settlement_reconciliations sr
JOIN claim_receipt_log_entries e ON e.receipt_id = sr.receipt_id
WHERE e.entry_seq <= kc.batch_end_seq
AND sr.reconciliation_state IN ('open', 'retry_scheduled')
)
AND NOT EXISTS (
SELECT 1 FROM metered_billing_reconciliations mbr
JOIN claim_receipt_log_entries e ON e.receipt_id = mbr.receipt_id
WHERE e.entry_seq <= kc.batch_end_seq
AND mbr.reconciliation_state IN ('open', 'retry_scheduled')
)
{active_settlement_guard}
"#
);
let watermark: i64 = connection.query_row(&query, params![cutoff], |row| row.get(0))?;
sqlite_u64(watermark, "retention watermark")
}
fn resolve_rotation_cutoff(
connection: &rusqlite::Connection,
config: &RetentionConfig,
) -> Result<Option<u64>, ReceiptStoreError> {
if let Some(cutoff) = config.explicit_cutoff_unix_secs {
return Ok(Some(cutoff));
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let time_cutoff = now.saturating_sub(config.retention_days.saturating_mul(86_400));
let oldest: Option<i64> = connection.query_row(
"SELECT MIN(timestamp) FROM claim_receipt_log_entries",
[],
|row| row.get::<_, Option<i64>>(0),
)?;
let mut cutoff: Option<u64> = None;
if let Some(oldest_ts) = oldest {
if (oldest_ts.max(0) as u64) < time_cutoff {
cutoff = Some(time_cutoff);
}
}
let size = live_db_size_bytes_on_connection(connection)?;
if size > config.max_size_bytes {
let median: Option<i64> = connection
.query_row(
"SELECT timestamp FROM claim_receipt_log_entries ORDER BY timestamp \
LIMIT 1 OFFSET (SELECT COUNT(*) FROM claim_receipt_log_entries) / 2",
[],
|row| row.get(0),
)
.optional()?;
if let Some(median_ts) = median {
let size_cutoff = (median_ts.max(0) as u64).saturating_add(1);
cutoff = Some(cutoff.map_or(size_cutoff, |current| current.max(size_cutoff)));
}
}
Ok(cutoff)
}
fn live_db_size_bytes_on_connection(
connection: &rusqlite::Connection,
) -> Result<u64, ReceiptStoreError> {
let page_count: i64 = connection.query_row("PRAGMA page_count", [], |row| row.get(0))?;
let freelist_count: i64 =
connection.query_row("PRAGMA freelist_count", [], |row| row.get(0))?;
let page_size: i64 = connection.query_row("PRAGMA page_size", [], |row| row.get(0))?;
let live_pages = (page_count - freelist_count).max(0);
Ok((live_pages as u64) * (page_size.max(0) as u64))
}
fn ensure_archive_file_exists(archive_path: &str) -> Result<(), ReceiptStoreError> {
let flags = rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE
| rusqlite::OpenFlags::SQLITE_OPEN_CREATE
| rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX;
let connection = rusqlite::Connection::open_with_flags(archive_path, flags)?;
drop(connection);
Ok(())
}
fn absolute_archive_path(archive_path: &str) -> Result<String, ReceiptStoreError> {
let canonical = std::fs::canonicalize(std::path::Path::new(archive_path)).map_err(|error| {
ReceiptStoreError::Conflict(format!(
"retention archive path {archive_path:?} could not be resolved to an absolute path: \
{error}"
))
})?;
canonical.to_str().map(str::to_owned).ok_or_else(|| {
ReceiptStoreError::Conflict(format!(
"retention archive path {archive_path:?} is not valid UTF-8 after canonicalization"
))
})
}
fn ensure_durable_distinct_archive_path(
connection: &rusqlite::Connection,
archive_path: &str,
) -> Result<(), ReceiptStoreError> {
let trimmed = archive_path.trim();
let lowered = trimmed.to_ascii_lowercase();
if trimmed.is_empty() || lowered == ":memory:" || lowered.contains("mode=memory") {
return Err(ReceiptStoreError::Conflict(format!(
"retention archive path {archive_path:?} is not a durable database file; refusing to \
co-archive evidence into a target destroyed on detach"
)));
}
let main_file: String = connection
.query_row(
"SELECT file FROM pragma_database_list WHERE name = 'main'",
[],
|row| row.get::<_, Option<String>>(0),
)
.optional()?
.flatten()
.unwrap_or_default();
if !main_file.is_empty() && archive_path_aliases_live(&main_file, trimmed) {
return Err(ReceiptStoreError::Conflict(format!(
"retention archive path {archive_path:?} aliases the live database; refusing to archive \
a store into itself"
)));
}
Ok(())
}
fn ensure_archive_path_matches_ledger(
connection: &rusqlite::Connection,
archive_path: &str,
) -> Result<(), ReceiptStoreError> {
if let Some(recorded) = super::support::latest_watermark_archive_path(connection)? {
if recorded != archive_path {
return Err(ReceiptStoreError::Conflict(format!(
"retention archive path {archive_path:?} differs from the archive {recorded:?} an \
earlier rotation committed to; the archived prefix would be split across two \
files. Keep the same archive path or start a fresh store"
)));
}
}
Ok(())
}
fn ensure_committed_prefix_still_backed(
connection: &rusqlite::Connection,
) -> Result<(), ReceiptStoreError> {
let Some(current) = super::support::retention_watermark(connection)? else {
return Ok(());
};
if current == 0 {
return Ok(());
}
let backed = match super::support::latest_watermark_archive_path(connection)? {
Some(ledger_path) => {
super::support::archive_path_backs_prefix(connection, &ledger_path, current)?
}
None => false,
};
if !backed {
return Err(ReceiptStoreError::Conflict(format!(
"retention archive no longer backs the committed watermark {current}; the prior \
archive is missing, unreadable, or does not re-derive the signed checkpoint roots. \
Refusing to rotate and strand the deleted prefix; restore the archive before \
rotating again"
)));
}
Ok(())
}
fn archive_path_aliases_live(main_file: &str, archive_path: &str) -> bool {
if main_file == archive_path {
return true;
}
let resolved = |path: &str| std::fs::canonicalize(std::path::Path::new(path)).ok();
if let (Some(main), Some(archive)) = (resolved(main_file), resolved(archive_path)) {
if main == archive {
return true;
}
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if let (Ok(main), Ok(archive)) = (
std::fs::metadata(main_file),
std::fs::metadata(archive_path),
) {
if main.dev() == archive.dev() && main.ino() == archive.ino() {
return true;
}
}
}
false
}
pub(super) fn rotate_on_writer_connection(
connection: &mut rusqlite::Connection,
config: &RetentionConfig,
verified_checkpoint_ceiling: Option<u64>,
) -> Result<u64, ReceiptStoreError> {
if config.tenant_id.is_some() {
return Err(ReceiptStoreError::RetentionTenantScopeUnsupported);
}
super::support::migrate_auto_vacuum_incremental_if_needed(connection)?;
super::support::ensure_receipt_retention_tombstones(connection)?;
super::support::ensure_receipt_retention_watermark_table(connection)?;
let Some(cutoff) = resolve_rotation_cutoff(connection, config)? else {
return Ok(0);
};
archive_range(
connection,
cutoff,
&config.archive_path,
verified_checkpoint_ceiling,
)
}
fn archive_range(
connection: &mut rusqlite::Connection,
cutoff_unix_secs: u64,
archive_path: &str,
verified_checkpoint_ceiling: Option<u64>,
) -> Result<u64, ReceiptStoreError> {
let watermark = compute_archival_watermark(connection, cutoff_unix_secs)?
.min(verified_checkpoint_ceiling.unwrap_or(u64::MAX));
if watermark == 0 {
return Ok(0); }
if let Some(current) = super::support::retention_watermark(connection)? {
if watermark <= current {
return Ok(0);
}
}
let w = sqlite_i64(watermark, "archival watermark")?;
ensure_durable_distinct_archive_path(connection, archive_path)?;
ensure_archive_file_exists(archive_path)?;
let archive_path = absolute_archive_path(archive_path)?;
ensure_archive_path_matches_ledger(connection, &archive_path)?;
ensure_committed_prefix_still_backed(connection)?;
let escaped_path = archive_path.replace('\'', "''");
connection.execute_batch(&format!("ATTACH DATABASE '{escaped_path}' AS archive"))?;
let result = (|| -> Result<u64, ReceiptStoreError> {
create_archive_schema(connection)?;
let archived = copy_archived_prefix(connection, w)?;
verify_co_archival_complete(connection, w)?; delete_archived_prefix_in_tx(connection, w, cutoff_unix_secs, &archive_path)?;
Ok(archived)
})();
let detach = connection.execute_batch("DETACH DATABASE archive");
let archived = match (result, detach) {
(Ok(archived), Ok(())) => archived,
(Err(error), _) => return Err(error),
(Ok(_), Err(error)) => return Err(error.into()),
};
connection.execute_batch("PRAGMA incremental_vacuum")?;
connection.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(archived)
}
pub(super) fn create_archive_schema(
connection: &mut rusqlite::Connection,
) -> Result<(), ReceiptStoreError> {
let transaction =
connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
let application_id: i32 =
transaction.query_row("PRAGMA archive.application_id", [], |row| row.get(0))?;
if application_id != 0 && application_id != crate::CHIO_SQLITE_APPLICATION_ID {
return Err(ReceiptStoreError::Conflict(format!(
"retention archive application_id {application_id:#x} is not a Chio store"
)));
}
let version_table_exists: bool = transaction.query_row(
"SELECT EXISTS(SELECT 1 FROM archive.sqlite_master WHERE type = 'table' AND name = 'chio_store_schema_versions')",
[],
|row| row.get(0),
)?;
let archive_schema_version = if version_table_exists {
transaction
.query_row(
"SELECT version FROM archive.chio_store_schema_versions WHERE store_key = ?1",
[RECEIPT_STORE_SCHEMA_KEY],
|row| row.get::<_, i32>(0),
)
.optional()?
.unwrap_or(0)
} else {
0
};
if archive_schema_version > RECEIPT_STORE_SUPPORTED_SCHEMA_VERSION {
return Err(ReceiptStoreError::Conflict(format!(
"retention archive schema version {archive_schema_version} is newer than this binary supports ({RECEIPT_STORE_SUPPORTED_SCHEMA_VERSION})"
)));
}
transaction.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS archive.chio_tool_receipts (
seq INTEGER PRIMARY KEY,
receipt_id TEXT NOT NULL UNIQUE, timestamp INTEGER NOT NULL,
capability_id TEXT NOT NULL, subject_key TEXT, issuer_key TEXT,
grant_index INTEGER, tool_server TEXT NOT NULL, tool_name TEXT NOT NULL,
decision_kind TEXT NOT NULL, policy_hash TEXT NOT NULL,
content_hash TEXT NOT NULL, raw_json TEXT NOT NULL, tenant_id TEXT,
cost_currency TEXT CHECK (
cost_currency IS NULL OR (
typeof(cost_currency) = 'text' AND
length(cost_currency) = 3 AND
cost_currency NOT GLOB '*[^A-Z]*'
)
),
cost_charged_be BLOB CHECK (
(cost_currency IS NULL AND cost_charged_be IS NULL) OR
(
cost_currency IS NOT NULL AND
typeof(cost_charged_be) = 'blob' AND
length(cost_charged_be) = 8
)
)
);
CREATE TABLE IF NOT EXISTS archive.chio_child_receipts (
seq INTEGER PRIMARY KEY,
receipt_id TEXT NOT NULL UNIQUE, timestamp INTEGER NOT NULL,
session_id TEXT NOT NULL, parent_request_id TEXT NOT NULL,
request_id TEXT NOT NULL, operation_kind TEXT NOT NULL,
terminal_state TEXT NOT NULL, policy_hash TEXT NOT NULL,
outcome_hash TEXT NOT NULL, raw_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS archive.kernel_checkpoints (
id INTEGER PRIMARY KEY, checkpoint_seq INTEGER NOT NULL UNIQUE,
batch_start_seq INTEGER NOT NULL, batch_end_seq INTEGER NOT NULL,
tree_size INTEGER NOT NULL, merkle_root TEXT NOT NULL,
issued_at INTEGER NOT NULL, statement_json TEXT NOT NULL,
signature TEXT NOT NULL, kernel_key TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS archive.capability_lineage (
capability_id TEXT PRIMARY KEY, subject_key TEXT NOT NULL,
issuer_key TEXT NOT NULL, issued_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL, grants_json TEXT NOT NULL,
delegation_depth INTEGER NOT NULL DEFAULT 0, parent_capability_id TEXT,
federated_parent_capability_id TEXT,
provenance TEXT NOT NULL DEFAULT 'legacy_projection'
CHECK (provenance IN ('signed_token', 'synthetic_anchor', 'legacy_projection')),
signed_capability_json TEXT
);
CREATE TABLE IF NOT EXISTS archive.claim_receipt_log_entries (
entry_seq INTEGER PRIMARY KEY,
receipt_id TEXT NOT NULL UNIQUE, receipt_kind TEXT NOT NULL,
source_seq INTEGER NOT NULL, timestamp INTEGER NOT NULL,
capability_id TEXT, session_id TEXT, parent_request_id TEXT,
request_id TEXT, subject_key TEXT, issuer_key TEXT,
tool_server TEXT, tool_name TEXT, raw_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS archive.settlement_reconciliations (
receipt_id TEXT PRIMARY KEY, reconciliation_state TEXT NOT NULL,
note TEXT, updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS archive.metered_billing_reconciliations (
receipt_id TEXT PRIMARY KEY, adapter_kind TEXT NOT NULL,
evidence_id TEXT NOT NULL, observed_units INTEGER NOT NULL,
billed_cost_units INTEGER NOT NULL, billed_cost_currency TEXT NOT NULL,
evidence_sha256 TEXT, recorded_at INTEGER NOT NULL,
reconciliation_state TEXT NOT NULL, note TEXT, updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS archive.chio_authorization_receipt_consumptions (
authorization_receipt_id TEXT PRIMARY KEY, consumer_receipt_id TEXT NOT NULL,
request_id TEXT NOT NULL, session_id TEXT NOT NULL, tool_call_id TEXT NOT NULL,
tenant_id TEXT, parameter_hash TEXT NOT NULL, consumed_at_unix_ms INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS archive.receipt_lineage_statements (
receipt_id TEXT PRIMARY KEY, statement_id TEXT, request_id TEXT,
session_id TEXT, session_anchor_id TEXT, chain_id TEXT,
parent_request_id TEXT, parent_receipt_id TEXT, evidence_class TEXT,
evidence_sources_json TEXT,
verified_session_anchor INTEGER NOT NULL DEFAULT 0,
verified_parent_request INTEGER NOT NULL DEFAULT 0,
verified_parent_receipt INTEGER NOT NULL DEFAULT 0,
replay_protected INTEGER NOT NULL DEFAULT 0,
recorded_at INTEGER NOT NULL, source_kind TEXT NOT NULL,
json_sha256 TEXT NOT NULL, raw_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS archive.checkpoint_tree_heads (
checkpoint_seq INTEGER PRIMARY KEY, batch_start_seq INTEGER NOT NULL,
batch_end_seq INTEGER NOT NULL, tree_size INTEGER NOT NULL,
merkle_root TEXT NOT NULL, issued_at INTEGER NOT NULL, kernel_key TEXT NOT NULL,
previous_checkpoint_sha256 TEXT, statement_json TEXT NOT NULL, signature TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS archive.checkpoint_predecessor_witnesses (
predecessor_checkpoint_seq INTEGER NOT NULL, witness_checkpoint_seq INTEGER PRIMARY KEY,
previous_checkpoint_sha256 TEXT NOT NULL, witnessed_at INTEGER NOT NULL,
witness_statement_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS archive.checkpoint_publication_metadata (
checkpoint_seq INTEGER PRIMARY KEY, publication_schema TEXT NOT NULL,
merkle_root TEXT NOT NULL, published_at INTEGER NOT NULL, kernel_key TEXT NOT NULL,
log_tree_size INTEGER NOT NULL, entry_start_seq INTEGER NOT NULL,
entry_end_seq INTEGER NOT NULL, previous_checkpoint_sha256 TEXT
);
CREATE TABLE IF NOT EXISTS archive.checkpoint_publication_trust_anchor_bindings (
checkpoint_seq INTEGER PRIMARY KEY, binding_json TEXT NOT NULL
);
"#,
)?;
if archive_schema_version < RECEIPT_COST_PROJECTION_SCHEMA_VERSION {
migrate_archive_receipt_cost_projection(&transaction)?;
}
verify_archive_receipt_cost_projection(&transaction)?;
for (column, definition) in [
("signed_capability_json", "signed_capability_json TEXT"),
(
"federated_parent_capability_id",
"federated_parent_capability_id TEXT",
),
(
"provenance",
"provenance TEXT NOT NULL DEFAULT 'legacy_projection' CHECK (provenance IN \
('signed_token', 'synthetic_anchor', 'legacy_projection'))",
),
] {
let has_column = {
let mut statement =
transaction.prepare("PRAGMA archive.table_info(capability_lineage)")?;
let mut rows = statement.query([])?;
let mut found = false;
while let Some(row) = rows.next()? {
if row.get::<_, String>(1)? == column {
found = true;
break;
}
}
found
};
if !has_column {
transaction.execute(
&format!("ALTER TABLE archive.capability_lineage ADD COLUMN {definition}"),
[],
)?;
}
}
transaction.execute(
"UPDATE archive.capability_lineage SET provenance = 'signed_token' \
WHERE provenance = 'legacy_projection' \
AND signed_capability_json IS NOT NULL",
[],
)?;
transaction.execute_batch(&format!(
"PRAGMA archive.application_id = {}; \
CREATE TABLE IF NOT EXISTS archive.chio_store_schema_versions (\
store_key TEXT PRIMARY KEY, version INTEGER NOT NULL\
);",
crate::CHIO_SQLITE_APPLICATION_ID
))?;
transaction.execute(
"INSERT INTO archive.chio_store_schema_versions (store_key, version) VALUES (?1, ?2) \
ON CONFLICT(store_key) DO UPDATE SET version = excluded.version",
params![
RECEIPT_STORE_SCHEMA_KEY,
RECEIPT_STORE_SUPPORTED_SCHEMA_VERSION
],
)?;
transaction.commit()?;
Ok(())
}
pub(super) fn copy_archived_prefix(
connection: &rusqlite::Connection,
w: i64,
) -> Result<u64, ReceiptStoreError> {
let before: i64 = connection.query_row(
"SELECT COUNT(*) FROM archive.chio_tool_receipts",
[],
|row| row.get(0),
)?;
connection.execute_batch(&format!(
r#"
INSERT OR IGNORE INTO archive.claim_receipt_log_entries
SELECT * FROM main.claim_receipt_log_entries WHERE entry_seq <= {w};
INSERT OR IGNORE INTO archive.chio_tool_receipts
(seq, receipt_id, timestamp, capability_id, subject_key, issuer_key,
grant_index, tool_server, tool_name, decision_kind, policy_hash,
content_hash, raw_json, tenant_id, cost_currency, cost_charged_be)
SELECT seq, receipt_id, timestamp, capability_id, subject_key, issuer_key,
grant_index, tool_server, tool_name, decision_kind, policy_hash,
content_hash, raw_json, tenant_id, cost_currency, cost_charged_be
FROM main.chio_tool_receipts WHERE seq IN (
SELECT source_seq FROM main.claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
INSERT OR IGNORE INTO archive.chio_child_receipts
(seq, receipt_id, timestamp, session_id, parent_request_id, request_id,
operation_kind, terminal_state, policy_hash, outcome_hash, raw_json)
SELECT seq, receipt_id, timestamp, session_id, parent_request_id, request_id,
operation_kind, terminal_state, policy_hash, outcome_hash, raw_json
FROM main.chio_child_receipts WHERE seq IN (
SELECT source_seq FROM main.claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'child_receipt');
INSERT OR IGNORE INTO archive.kernel_checkpoints
SELECT * FROM main.kernel_checkpoints WHERE batch_end_seq <= {w};
INSERT OR IGNORE INTO archive.capability_lineage
(capability_id, subject_key, issuer_key, issued_at, expires_at,
grants_json, delegation_depth, parent_capability_id,
federated_parent_capability_id, provenance, signed_capability_json)
SELECT DISTINCT cl.capability_id, cl.subject_key, cl.issuer_key,
cl.issued_at, cl.expires_at, cl.grants_json,
cl.delegation_depth, cl.parent_capability_id,
cl.federated_parent_capability_id, cl.provenance,
cl.signed_capability_json
FROM main.capability_lineage cl
INNER JOIN main.chio_tool_receipts r ON r.capability_id = cl.capability_id
WHERE r.seq IN (
SELECT source_seq FROM main.claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
-- Refresh (not IGNORE) the mutable reconciliation rows: an earlier copy
-- may hold pre-update bytes for a receipt whose reconciliation state has
-- since advanced, and only replacing it lets a retried rotation pass the
-- write-locked co-archival re-verify instead of stalling on stale bytes.
INSERT OR REPLACE INTO archive.settlement_reconciliations
SELECT * FROM main.settlement_reconciliations WHERE receipt_id IN (
SELECT receipt_id FROM main.claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
INSERT OR REPLACE INTO archive.metered_billing_reconciliations
SELECT * FROM main.metered_billing_reconciliations WHERE receipt_id IN (
SELECT receipt_id FROM main.claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
INSERT OR IGNORE INTO archive.chio_authorization_receipt_consumptions
SELECT * FROM main.chio_authorization_receipt_consumptions WHERE authorization_receipt_id IN (
SELECT receipt_id FROM main.claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
INSERT OR IGNORE INTO archive.receipt_lineage_statements
(receipt_id, statement_id, request_id, session_id, session_anchor_id,
chain_id, parent_request_id, parent_receipt_id, evidence_class,
evidence_sources_json, verified_session_anchor, verified_parent_request,
verified_parent_receipt, replay_protected, recorded_at, source_kind,
json_sha256, raw_json)
SELECT receipt_id, statement_id, request_id, session_id, session_anchor_id,
chain_id, parent_request_id, parent_receipt_id, evidence_class,
evidence_sources_json, verified_session_anchor, verified_parent_request,
verified_parent_receipt, replay_protected, recorded_at, source_kind,
json_sha256, raw_json
FROM main.receipt_lineage_statements WHERE receipt_id IN (
SELECT receipt_id FROM main.claim_receipt_log_entries WHERE entry_seq <= {w});
"#
))?;
let after: i64 = connection.query_row(
"SELECT COUNT(*) FROM archive.chio_tool_receipts",
[],
|row| row.get(0),
)?;
sqlite_u64((after - before).max(0), "archived tool receipt delta")
}
fn verify_co_archival_complete(
connection: &rusqlite::Connection,
w: i64,
) -> Result<(), ReceiptStoreError> {
let checks: [(&'static str, String, String); 9] = [
(
"chio_tool_receipts",
format!(
"SELECT COUNT(*) FROM main.chio_tool_receipts WHERE seq IN \
(SELECT source_seq FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt')"
),
format!(
"SELECT COUNT(*) FROM main.chio_tool_receipts m WHERE m.seq IN \
(SELECT source_seq FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt') \
AND EXISTS (SELECT 1 FROM archive.chio_tool_receipts a WHERE a.seq = m.seq \
AND a.receipt_id IS m.receipt_id AND a.timestamp IS m.timestamp \
AND a.capability_id IS m.capability_id AND a.subject_key IS m.subject_key \
AND a.issuer_key IS m.issuer_key AND a.grant_index IS m.grant_index \
AND a.tool_server IS m.tool_server AND a.tool_name IS m.tool_name \
AND a.decision_kind IS m.decision_kind AND a.policy_hash IS m.policy_hash \
AND a.content_hash IS m.content_hash AND a.raw_json IS m.raw_json \
AND a.tenant_id IS m.tenant_id \
AND a.cost_currency IS m.cost_currency \
AND a.cost_charged_be IS m.cost_charged_be)"
),
),
(
"chio_child_receipts",
format!(
"SELECT COUNT(*) FROM main.chio_child_receipts WHERE seq IN \
(SELECT source_seq FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'child_receipt')"
),
format!(
"SELECT COUNT(*) FROM main.chio_child_receipts m WHERE m.seq IN \
(SELECT source_seq FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'child_receipt') \
AND EXISTS (SELECT 1 FROM archive.chio_child_receipts a WHERE a.seq = m.seq \
AND a.receipt_id IS m.receipt_id AND a.timestamp IS m.timestamp \
AND a.session_id IS m.session_id AND a.parent_request_id IS m.parent_request_id \
AND a.request_id IS m.request_id AND a.operation_kind IS m.operation_kind \
AND a.terminal_state IS m.terminal_state AND a.policy_hash IS m.policy_hash \
AND a.outcome_hash IS m.outcome_hash AND a.raw_json IS m.raw_json)"
),
),
(
"claim_receipt_log_entries",
format!("SELECT COUNT(*) FROM main.claim_receipt_log_entries WHERE entry_seq <= {w}"),
format!(
"SELECT COUNT(*) FROM main.claim_receipt_log_entries m WHERE m.entry_seq <= {w} \
AND EXISTS (SELECT 1 FROM archive.claim_receipt_log_entries a WHERE a.entry_seq = m.entry_seq \
AND a.receipt_id IS m.receipt_id AND a.receipt_kind IS m.receipt_kind \
AND a.source_seq IS m.source_seq AND a.timestamp IS m.timestamp \
AND a.capability_id IS m.capability_id AND a.session_id IS m.session_id \
AND a.parent_request_id IS m.parent_request_id AND a.request_id IS m.request_id \
AND a.subject_key IS m.subject_key AND a.issuer_key IS m.issuer_key \
AND a.tool_server IS m.tool_server AND a.tool_name IS m.tool_name \
AND a.raw_json IS m.raw_json)"
),
),
(
"kernel_checkpoints",
format!("SELECT COUNT(*) FROM main.kernel_checkpoints WHERE batch_end_seq <= {w}"),
format!(
"SELECT COUNT(*) FROM main.kernel_checkpoints m WHERE m.batch_end_seq <= {w} \
AND EXISTS (SELECT 1 FROM archive.kernel_checkpoints a WHERE a.checkpoint_seq = m.checkpoint_seq \
AND a.id IS m.id AND a.batch_start_seq IS m.batch_start_seq \
AND a.batch_end_seq IS m.batch_end_seq AND a.tree_size IS m.tree_size \
AND a.merkle_root IS m.merkle_root AND a.issued_at IS m.issued_at \
AND a.statement_json IS m.statement_json AND a.signature IS m.signature \
AND a.kernel_key IS m.kernel_key)"
),
),
(
"settlement_reconciliations",
format!(
"SELECT COUNT(*) FROM main.settlement_reconciliations WHERE receipt_id IN \
(SELECT receipt_id FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt')"
),
format!(
"SELECT COUNT(*) FROM main.settlement_reconciliations m WHERE m.receipt_id IN \
(SELECT receipt_id FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt') \
AND EXISTS (SELECT 1 FROM archive.settlement_reconciliations a WHERE a.receipt_id = m.receipt_id \
AND a.reconciliation_state IS m.reconciliation_state AND a.note IS m.note \
AND a.updated_at IS m.updated_at)"
),
),
(
"metered_billing_reconciliations",
format!(
"SELECT COUNT(*) FROM main.metered_billing_reconciliations WHERE receipt_id IN \
(SELECT receipt_id FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt')"
),
format!(
"SELECT COUNT(*) FROM main.metered_billing_reconciliations m WHERE m.receipt_id IN \
(SELECT receipt_id FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt') \
AND EXISTS (SELECT 1 FROM archive.metered_billing_reconciliations a WHERE a.receipt_id = m.receipt_id \
AND a.adapter_kind IS m.adapter_kind AND a.evidence_id IS m.evidence_id \
AND a.observed_units IS m.observed_units AND a.billed_cost_units IS m.billed_cost_units \
AND a.billed_cost_currency IS m.billed_cost_currency AND a.evidence_sha256 IS m.evidence_sha256 \
AND a.recorded_at IS m.recorded_at AND a.reconciliation_state IS m.reconciliation_state \
AND a.note IS m.note AND a.updated_at IS m.updated_at)"
),
),
(
"chio_authorization_receipt_consumptions",
format!(
"SELECT COUNT(*) FROM main.chio_authorization_receipt_consumptions WHERE authorization_receipt_id IN \
(SELECT receipt_id FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt')"
),
format!(
"SELECT COUNT(*) FROM main.chio_authorization_receipt_consumptions m WHERE m.authorization_receipt_id IN \
(SELECT receipt_id FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt') \
AND EXISTS (SELECT 1 FROM archive.chio_authorization_receipt_consumptions a \
WHERE a.authorization_receipt_id = m.authorization_receipt_id \
AND a.consumer_receipt_id IS m.consumer_receipt_id AND a.request_id IS m.request_id \
AND a.session_id IS m.session_id AND a.tool_call_id IS m.tool_call_id \
AND a.tenant_id IS m.tenant_id AND a.parameter_hash IS m.parameter_hash \
AND a.consumed_at_unix_ms IS m.consumed_at_unix_ms)"
),
),
(
"capability_lineage",
format!(
"SELECT COUNT(*) FROM main.capability_lineage m WHERE m.capability_id IN \
(SELECT r.capability_id FROM main.chio_tool_receipts r WHERE r.seq IN \
(SELECT source_seq FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt'))"
),
format!(
"SELECT COUNT(*) FROM main.capability_lineage m WHERE m.capability_id IN \
(SELECT r.capability_id FROM main.chio_tool_receipts r WHERE r.seq IN \
(SELECT source_seq FROM main.claim_receipt_log_entries WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt')) \
AND EXISTS (SELECT 1 FROM archive.capability_lineage a WHERE a.capability_id = m.capability_id \
AND a.subject_key IS m.subject_key AND a.issuer_key IS m.issuer_key \
AND a.issued_at IS m.issued_at AND a.expires_at IS m.expires_at \
AND a.grants_json IS m.grants_json AND a.delegation_depth IS m.delegation_depth \
AND a.parent_capability_id IS m.parent_capability_id \
AND a.federated_parent_capability_id IS m.federated_parent_capability_id \
AND a.provenance IS m.provenance \
AND a.signed_capability_json IS m.signed_capability_json)"
),
),
(
"receipt_lineage_statements",
format!(
"SELECT COUNT(*) FROM main.receipt_lineage_statements WHERE receipt_id IN \
(SELECT receipt_id FROM main.claim_receipt_log_entries WHERE entry_seq <= {w})"
),
format!(
"SELECT COUNT(*) FROM main.receipt_lineage_statements m WHERE m.receipt_id IN \
(SELECT receipt_id FROM main.claim_receipt_log_entries WHERE entry_seq <= {w}) \
AND EXISTS (SELECT 1 FROM archive.receipt_lineage_statements a WHERE a.receipt_id = m.receipt_id \
AND a.statement_id IS m.statement_id AND a.request_id IS m.request_id \
AND a.session_id IS m.session_id AND a.session_anchor_id IS m.session_anchor_id \
AND a.chain_id IS m.chain_id AND a.parent_request_id IS m.parent_request_id \
AND a.parent_receipt_id IS m.parent_receipt_id AND a.evidence_class IS m.evidence_class \
AND a.evidence_sources_json IS m.evidence_sources_json \
AND a.verified_session_anchor IS m.verified_session_anchor \
AND a.verified_parent_request IS m.verified_parent_request \
AND a.verified_parent_receipt IS m.verified_parent_receipt \
AND a.replay_protected IS m.replay_protected AND a.recorded_at IS m.recorded_at \
AND a.source_kind IS m.source_kind AND a.json_sha256 IS m.json_sha256 \
AND a.raw_json IS m.raw_json)"
),
),
];
for (table, live_sql, archive_sql) in checks {
let live: i64 = connection.query_row(&live_sql, [], |row| row.get(0))?;
let archived: i64 = connection.query_row(&archive_sql, [], |row| row.get(0))?;
if archived < live {
return Err(ReceiptStoreError::RetentionArchiveIncomplete {
table,
live: sqlite_u64(live, "live co-archival count")?,
archived: sqlite_u64(archived, "archive co-archival count")?,
});
}
}
Ok(())
}
pub(super) fn delete_archived_prefix_in_tx(
connection: &mut rusqlite::Connection,
w: i64,
cutoff_unix_secs: u64,
archive_path: &str,
) -> Result<(), ReceiptStoreError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
verify_co_archival_complete(&tx, w)?;
ensure_archive_path_matches_ledger(&tx, archive_path)?;
ensure_committed_prefix_still_backed(&tx)?;
tx.execute_batch(&format!(
r#"
DROP TRIGGER IF EXISTS chio_tool_receipts_reject_delete;
DROP TRIGGER IF EXISTS chio_child_receipts_reject_delete;
DROP TRIGGER IF EXISTS claim_receipt_log_entries_reject_delete;
-- Tombstone every archived receipt id BEFORE its claim-log row is
-- deleted so the append path still rejects a reused id once its live
-- UNIQUE(receipt_id) sentinel is gone. Idempotent for a re-run.
INSERT OR IGNORE INTO receipt_retention_tombstones
(receipt_id, receipt_kind, archived_through_entry_seq, tombstoned_at)
SELECT receipt_id, receipt_kind, {w}, {now}
FROM claim_receipt_log_entries WHERE entry_seq <= {w};
-- `compute_archival_watermark` never advances W past a receipt whose
-- settlement or metered-billing reconciliation is still nonterminal, so
-- every reconciliation row removed here has already reached a terminal
-- state and its co-archived copy preserves the closed record. No live,
-- actionable reconciliation loses the receipt its upsert path resolves.
DELETE FROM settlement_reconciliations WHERE receipt_id IN (
SELECT receipt_id FROM claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
DELETE FROM metered_billing_reconciliations WHERE receipt_id IN (
SELECT receipt_id FROM claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
-- Safe to key this delete on the archived authorization alone:
-- `compute_archival_watermark` never advances W past an authorization
-- whose bound consumer is still live, so every consumption removed here
-- has both its authorization and (when the consumer is a live receipt)
-- its consumer inside the archived prefix. No live consumer loses its
-- binding, and the co-archived copy preserves the archived pair.
DELETE FROM chio_authorization_receipt_consumptions WHERE authorization_receipt_id IN (
SELECT receipt_id FROM claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
DELETE FROM chio_tool_receipts WHERE seq IN (
SELECT source_seq FROM claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
DELETE FROM chio_child_receipts WHERE seq IN (
SELECT source_seq FROM claim_receipt_log_entries
WHERE entry_seq <= {w} AND receipt_kind = 'child_receipt');
DELETE FROM claim_receipt_log_entries WHERE entry_seq <= {w};
"#
))?;
insert_receipt_retention_watermark(
&tx,
sqlite_u64(w, "watermark entry_seq")?,
cutoff_unix_secs,
archive_path,
None,
now,
)?;
ensure_transparency_projection_guards(&tx)?; tx.commit()?;
Ok(())
}
pub(super) fn retention_repair_on_writer(
connection: &mut rusqlite::Connection,
archive_path: &str,
) -> Result<u64, ReceiptStoreError> {
let extras: Vec<(i64, String)> = {
let mut stmt = connection.prepare(
"SELECT e.entry_seq, e.receipt_id FROM claim_receipt_log_entries e \
WHERE NOT EXISTS (SELECT 1 FROM chio_tool_receipts t WHERE t.receipt_id = e.receipt_id) \
AND NOT EXISTS (SELECT 1 FROM chio_child_receipts c WHERE c.receipt_id = e.receipt_id) \
ORDER BY e.entry_seq",
)?;
let rows = stmt.query_map([], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
})?;
let mut out = Vec::new();
for row in rows {
out.push(row?);
}
out
};
if extras.is_empty() {
return Ok(0);
}
let max_extra_entry_seq = extras.iter().map(|(seq, _)| *seq).max().unwrap_or(0);
ensure_durable_distinct_archive_path(connection, archive_path)?;
ensure_archive_file_exists(archive_path)?;
let archive_path = absolute_archive_path(archive_path)?;
let archive_path = archive_path.as_str();
let escaped = archive_path.replace('\'', "''");
connection.execute_batch(&format!("ATTACH DATABASE '{escaped}' AS archive"))?;
let assert_result = (|| -> Result<u64, ReceiptStoreError> {
for (entry_seq, _) in &extras {
let faithful: i64 = connection.query_row(
"SELECT COUNT(*) FROM main.claim_receipt_log_entries m \
WHERE m.entry_seq = ?1 \
AND EXISTS (SELECT 1 FROM archive.claim_receipt_log_entries a \
WHERE a.entry_seq = m.entry_seq \
AND a.receipt_id IS m.receipt_id AND a.receipt_kind IS m.receipt_kind \
AND a.source_seq IS m.source_seq AND a.timestamp IS m.timestamp \
AND a.capability_id IS m.capability_id AND a.session_id IS m.session_id \
AND a.parent_request_id IS m.parent_request_id AND a.request_id IS m.request_id \
AND a.subject_key IS m.subject_key AND a.issuer_key IS m.issuer_key \
AND a.tool_server IS m.tool_server AND a.tool_name IS m.tool_name \
AND a.raw_json IS m.raw_json)",
params![entry_seq],
|row| row.get(0),
)?;
if faithful == 0 {
return Err(ReceiptStoreError::RetentionArchiveIncomplete {
table: "claim_receipt_log_entries",
live: 1,
archived: 0,
});
}
}
let rounded: Option<i64> = connection.query_row(
"SELECT MIN(batch_end_seq) FROM kernel_checkpoints WHERE batch_end_seq >= ?1",
params![max_extra_entry_seq],
|row| row.get(0),
)?;
let rounded = rounded.ok_or_else(|| {
ReceiptStoreError::Conflict(
"retention repair: extra claim-log rows are not covered by any checkpoint; \
refusing to touch the uncheckpointed suffix"
.to_string(),
)
})?;
let live_in_prefix: i64 = connection.query_row(
"SELECT COUNT(*) FROM claim_receipt_log_entries e \
WHERE e.entry_seq <= ?1 \
AND (EXISTS (SELECT 1 FROM chio_tool_receipts t WHERE t.receipt_id = e.receipt_id) \
OR EXISTS (SELECT 1 FROM chio_child_receipts c WHERE c.receipt_id = e.receipt_id))",
params![rounded],
|row| row.get(0),
)?;
if live_in_prefix > 0 {
return Err(ReceiptStoreError::Conflict(
"retention repair: the checkpoint batch covering the orphaned rows still has \
live source receipts; refusing to watermark a partially archived batch"
.to_string(),
));
}
let archived_prefix: i64 = connection.query_row(
"SELECT COUNT(*) FROM archive.claim_receipt_log_entries WHERE entry_seq <= ?1",
params![rounded],
|row| row.get(0),
)?;
if archived_prefix < rounded {
return Err(ReceiptStoreError::RetentionArchiveIncomplete {
table: "claim_receipt_log_entries",
live: sqlite_u64(rounded, "repair rounded prefix width")?,
archived: sqlite_u64(archived_prefix.max(0), "repair archived prefix count")?,
});
}
let rounded_u64 = sqlite_u64(rounded, "repair rounded watermark")?;
let watermark_covers = matches!(
super::support::retention_watermark(connection)?,
Some(current) if current >= rounded_u64
);
if watermark_covers {
if let Some(ledger_archive) = super::support::latest_watermark_archive_path(connection)?
{
if ledger_archive != archive_path {
return Err(ReceiptStoreError::Conflict(format!(
"retention repair archive {archive_path:?} differs from the archive \
{ledger_archive:?} that backs the existing watermark; tombstones must be \
stamped from the archive that backs the prefix. Supply the ledger archive \
or start a fresh store"
)));
}
}
}
if !super::support::archive_path_backs_prefix(connection, archive_path, rounded_u64)? {
return Err(ReceiptStoreError::RetentionArchiveIncomplete {
table: "claim_receipt_log_entries",
live: rounded_u64,
archived: 0,
});
}
Ok(rounded_u64)
})();
let rounded_watermark = match assert_result {
Ok(w) => w,
Err(error) => {
let _ = connection.execute_batch("DETACH DATABASE archive");
return Err(error);
}
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let removed = extras.len() as u64;
let repair_result = (|| -> Result<(), ReceiptStoreError> {
let rounded_i64 = sqlite_i64(rounded_watermark, "repair rounded watermark")?;
let now_i64 = sqlite_i64(now, "repair tombstone timestamp")?;
let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
tx.execute_batch("DROP TRIGGER IF EXISTS claim_receipt_log_entries_reject_delete;")?;
super::support::ensure_receipt_retention_tombstones(&tx)?;
tx.execute(
"INSERT OR IGNORE INTO receipt_retention_tombstones \
(receipt_id, receipt_kind, archived_through_entry_seq, tombstoned_at) \
SELECT receipt_id, receipt_kind, ?1, ?2 \
FROM archive.claim_receipt_log_entries WHERE entry_seq <= ?3",
params![rounded_i64, now_i64, rounded_i64],
)?;
for (entry_seq, _) in &extras {
tx.execute(
"DELETE FROM claim_receipt_log_entries WHERE entry_seq = ?1",
params![entry_seq],
)?;
}
super::support::ensure_receipt_retention_watermark_table(&tx)?;
let watermark_covers = matches!(
super::support::retention_watermark(&tx)?,
Some(current) if current >= rounded_watermark
);
if !watermark_covers {
insert_receipt_retention_watermark(
&tx,
rounded_watermark,
now,
archive_path,
None,
now,
)?;
}
ensure_transparency_projection_guards(&tx)?;
tx.commit()?;
Ok(())
})();
let detach = connection.execute_batch("DETACH DATABASE archive");
match (repair_result, detach) {
(Ok(()), Ok(())) => {}
(Err(error), _) => return Err(error),
(Ok(()), Err(error)) => return Err(error.into()),
}
connection.execute_batch("PRAGMA incremental_vacuum")?;
connection.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")?;
Ok(removed)
}