Skip to main content

chio_store_sqlite/budget_store/
composite.rs

1use super::*;
2use rusqlite::{params, Connection, Transaction};
3
4mod cumulative_model;
5mod event_projection;
6mod model;
7mod transitions;
8
9pub(crate) use transitions::AdmissionCaptureBinding;
10mod validation;
11
12use cumulative_model::*;
13use model::*;
14use transitions::*;
15use validation::*;
16
17#[derive(Clone, Copy)]
18pub(crate) struct AdmissionAuthorizationBinding<'a> {
19    pub(crate) operation: &'a chio_kernel::admission_operation::AdmissionOperationV1,
20    pub(crate) recovery_lease: &'a chio_kernel::admission_operation::AdmissionRecoveryLease,
21    pub(crate) payment_journal: Option<&'a PaymentJournalRecord>,
22    pub(crate) credit_exposure:
23        Option<&'a chio_credit::obligation::CreditExposureReservationRequest>,
24    pub(crate) trusted_now_unix_ms: u64,
25}
26
27impl SqliteBudgetStore {
28    pub(crate) fn composite_quota_usage(
29        &self,
30        key: &BudgetQuotaKey,
31    ) -> Result<Option<BudgetInvocationQuotaUsage>, BudgetStoreError> {
32        let mut connection = self.connection()?;
33        let transaction = self.begin_read(&mut connection)?;
34        let usage = load_quota_state(&transaction, key)?.map(|state| state.usage());
35        transaction.rollback()?;
36        Ok(usage)
37    }
38
39    pub(crate) fn composite_cumulative_operation_usage(
40        &self,
41        operation_id: &str,
42    ) -> Result<Option<BudgetCumulativeApprovalUsage>, BudgetStoreError> {
43        self.cumulative_approval_operation_projection(operation_id)
44            .map(|projection| projection.map(|(usage, _)| usage))
45    }
46
47    /// Returns an operation's latest cumulative usage and durable mutation event.
48    pub fn cumulative_approval_operation_projection(
49        &self,
50        operation_id: &str,
51    ) -> Result<Option<(BudgetCumulativeApprovalUsage, BudgetMutationRecord)>, BudgetStoreError>
52    {
53        let mut connection = self.connection()?;
54        let transaction = self.begin_read(&mut connection)?;
55        let row = transaction
56            .query_row(
57                r#"
58                SELECT operation.operation_id, (
59                    SELECT event.event_id
60                    FROM budget_event_cumulative_approval AS cumulative
61                    JOIN budget_mutation_events AS event
62                      ON event.event_id = cumulative.event_id
63                    WHERE cumulative.operation_id = operation.operation_id
64                    ORDER BY event.event_seq DESC
65                    LIMIT 1
66                )
67                FROM budget_cumulative_approval_operations AS operation
68                WHERE operation.operation_id = ?1
69                "#,
70                params![operation_id],
71                |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?)),
72            )
73            .optional()?;
74        let Some((stored_operation_id, event_id)) = row else {
75            transaction.rollback()?;
76            return Ok(None);
77        };
78        let event_id = event_id.ok_or_else(|| {
79            BudgetStoreError::Invariant(format!(
80                "cumulative approval operation `{stored_operation_id}` has no durable event"
81            ))
82        })?;
83        let event =
84            Self::load_projected_mutation_event(&transaction, &event_id)?.ok_or_else(|| {
85                BudgetStoreError::Invariant(format!(
86                    "cumulative approval operation `{stored_operation_id}` lost its event"
87                ))
88            })?;
89        let usage = event
90            .cumulative_approval
91            .clone()
92            .filter(|usage| usage.operation_id == stored_operation_id)
93            .ok_or_else(|| {
94                BudgetStoreError::Invariant(format!(
95                    "cumulative approval operation `{stored_operation_id}` lost its projection"
96                ))
97            })?;
98        transaction.rollback()?;
99        Ok(Some((usage, event)))
100    }
101
102    pub(super) fn authorize_composite_hold(
103        &self,
104        request: BudgetAuthorizeHoldRequest,
105    ) -> Result<BudgetAuthorizeHoldDecision, BudgetStoreError> {
106        self.authorize_composite_hold_inner(request, None)
107            .map(|(decision, _)| decision)
108    }
109
110    pub(crate) fn authorize_composite_hold_and_commit_admission(
111        &self,
112        request: BudgetAuthorizeHoldRequest,
113        binding: AdmissionAuthorizationBinding<'_>,
114    ) -> Result<
115        (
116            BudgetAuthorizeHoldDecision,
117            chio_kernel::admission_operation::AdmissionOperationV1,
118        ),
119        BudgetStoreError,
120    > {
121        let (decision, operation) = self.authorize_composite_hold_inner(request, Some(binding))?;
122        let operation = operation.ok_or_else(|| {
123            BudgetStoreError::Invariant(
124                "combined budget authorization omitted its admission operation".to_owned(),
125            )
126        })?;
127        Ok((decision, operation))
128    }
129
130    fn authorize_composite_hold_inner(
131        &self,
132        request: BudgetAuthorizeHoldRequest,
133        binding: Option<AdmissionAuthorizationBinding<'_>>,
134    ) -> Result<
135        (
136            BudgetAuthorizeHoldDecision,
137            Option<chio_kernel::admission_operation::AdmissionOperationV1>,
138        ),
139        BudgetStoreError,
140    > {
141        request.validate()?;
142        let quotas = normalized_quotas(&request)?;
143        validate_composite_sqlite_range(&request, &quotas)?;
144        let hold_id = request.hold_id.as_deref().ok_or_else(|| {
145            BudgetStoreError::Invariant("composite budget hold_id is missing".to_string())
146        })?;
147        let event_id = request.event_id.as_deref().ok_or_else(|| {
148            BudgetStoreError::Invariant("composite budget event_id is missing".to_string())
149        })?;
150        let admission = request.admission_binding.as_ref().ok_or_else(|| {
151            BudgetStoreError::Invariant("composite admission binding is missing".to_string())
152        })?;
153
154        let mut connection = self.connection()?;
155        let transaction = self.begin_write(&mut connection)?;
156        self.validate_joint_authority(request.authority.as_ref())?;
157        if let Some(existing) = Self::load_mutation_event(&transaction, event_id)? {
158            let stored = load_authorization_request(&transaction, &existing)?;
159            self.validate_persisted_authority(
160                &transaction,
161                event_id,
162                stored.authority.as_ref(),
163                request.authority.as_ref(),
164            )?;
165            let mut normalized_request = request.clone();
166            normalized_request.authority = stored.authority.clone();
167            if self.serving_owner.is_some() {
168                normalized_request
169                    .admission_binding
170                    .as_mut()
171                    .ok_or_else(|| {
172                        BudgetStoreError::Invariant(
173                            "composite authorization replay lost admission binding".to_string(),
174                        )
175                    })?
176                    .last_observed_revocation = stored
177                    .admission_binding
178                    .as_ref()
179                    .and_then(|binding| binding.last_observed_revocation.clone());
180            }
181            if stored != normalized_request {
182                return Err(BudgetStoreError::Invariant(format!(
183                    "budget event_id `{event_id}` was reused for a different mutation"
184                )));
185            }
186            let decision = self.authorization_decision_from_event(&transaction, &existing)?;
187            let operation = self.bind_authorization_to_admission(
188                &transaction,
189                &request,
190                &decision,
191                binding,
192                false,
193            )?;
194            if binding.is_some() {
195                self.commit_joint_transaction(transaction)?;
196                self.sync_joint_anchor(&connection)?;
197            } else {
198                transaction.rollback()?;
199            }
200            return Ok((decision, operation));
201        }
202        let grant_quota_index = i64::try_from(request.grant_index).map_err(|_| {
203            BudgetStoreError::Invariant("grant_index does not fit sqlite range".to_string())
204        })?;
205        let grant_index = u32::try_from(request.grant_index).map_err(|_| {
206            BudgetStoreError::Invariant("grant_index does not fit quota range".to_string())
207        })?;
208        let durable_grant_quota_exists = transaction.query_row(
209            r#"
210            SELECT EXISTS(
211                SELECT 1 FROM budget_invocation_quotas
212                WHERE profile = 'chio.grant-invocation.v1'
213                  AND owner_id = ?1 AND grant_index = ?2
214            )
215            "#,
216            params![&request.capability_id, grant_quota_index],
217            |row| row.get::<_, bool>(0),
218        )?;
219        let request_includes_grant_quota = quotas.iter().any(|quota| {
220            quota.key == BudgetQuotaKey::grant(request.capability_id.clone(), grant_index)
221        });
222        let legacy_grant_limit =
223            legacy_grant_quota_limit(&transaction, &request.capability_id, request.grant_index)?;
224        if durable_grant_quota_exists && !request_includes_grant_quota {
225            return Err(BudgetStoreError::Invariant(format!(
226                "structured authorization omitted durable grant quota for `{}` grant {}",
227                request.capability_id, request.grant_index
228            )));
229        }
230        if let Some(legacy_limit) = legacy_grant_limit {
231            let requested = quotas.iter().find(|quota| {
232                quota.key == BudgetQuotaKey::grant(request.capability_id.clone(), grant_index)
233            });
234            if requested.map(|quota| quota.max_invocations) != Some(legacy_limit) {
235                return Err(BudgetStoreError::Invariant(format!(
236                    "structured authorization must preserve legacy grant quota {legacy_limit} for `{}` grant {}",
237                    request.capability_id, request.grant_index
238                )));
239            }
240        }
241        let hold_identity_exists = Self::load_hold(&transaction, hold_id)?.is_some()
242            || transaction.query_row(
243                "SELECT EXISTS(SELECT 1 FROM budget_mutation_events WHERE hold_id = ?1)",
244                params![hold_id],
245                |row| row.get::<_, bool>(0),
246            )?;
247        if hold_identity_exists {
248            return Err(BudgetStoreError::Invariant(format!(
249                "budget hold `{hold_id}` was reused for a different authorization"
250            )));
251        }
252        self.validate_observed_revocation(&transaction, admission)?;
253        let mut revoked_member = false;
254        if self.serving_owner.is_some() {
255            for capability_id in admission.revocation_set.ids() {
256                revoked_member |= transaction.query_row(
257                    "SELECT EXISTS(SELECT 1 FROM revoked_capabilities WHERE capability_id = ?1)",
258                    params![capability_id],
259                    |row| row.get::<_, bool>(0),
260                )?;
261            }
262        }
263        let operation_exists = transaction.query_row(
264            r#"
265            SELECT EXISTS(
266                SELECT 1 FROM budget_mutation_events
267                WHERE operation_id = ?1
268                  AND authorization_outcome IS NOT 'denied'
269            )
270            "#,
271            params![&admission.operation_id],
272            |row| row.get::<_, bool>(0),
273        )?;
274        if operation_exists {
275            return Err(BudgetStoreError::Invariant(format!(
276                "budget operation `{}` was reused for a different authorization",
277                admission.operation_id
278            )));
279        }
280
281        let current =
282            load_usage_or_default(&transaction, &request.capability_id, request.grant_index)?;
283        let committed = checked_committed_cost_units(
284            current.total_cost_exposed,
285            current.total_cost_realized_spend,
286        )?;
287        let requested_total = committed
288            .checked_add(request.requested_exposure_units)
289            .ok_or_else(|| {
290                BudgetStoreError::Overflow(
291                    "committed cost + requested exposure overflowed u64".to_string(),
292                )
293            })?;
294        let mut allowed = !revoked_member
295            && request
296                .max_cost_per_invocation
297                .is_none_or(|max| request.requested_exposure_units <= max)
298            && request
299                .max_total_cost_units
300                .is_none_or(|max| requested_total <= max);
301        let mut quota_before = Vec::with_capacity(quotas.len());
302        for quota in &quotas {
303            let state = load_quota_state(&transaction, &quota.key)?;
304            match state {
305                Some(state) => {
306                    if state.maximum != quota.max_invocations {
307                        return Err(BudgetStoreError::Invariant(format!(
308                            "budget quota `{}` maximum changed from {} to {}",
309                            quota.key.owner_id, state.maximum, quota.max_invocations
310                        )));
311                    }
312                    let used = state.reserved.checked_add(state.captured).ok_or_else(|| {
313                        BudgetStoreError::Overflow(
314                            "reserved + captured quota count overflowed u32".to_string(),
315                        )
316                    })?;
317                    allowed &= used < state.maximum;
318                    quota_before.push(state);
319                }
320                None => {
321                    let is_grant_quota = quota.key
322                        == BudgetQuotaKey::grant(request.capability_id.clone(), grant_index);
323                    let mut state = QuotaState::new(quota);
324                    if is_grant_quota && current.invocation_count != 0 {
325                        if legacy_grant_limit != Some(quota.max_invocations) {
326                            return Err(BudgetStoreError::Invariant(
327                                "cannot define a grant quota after untracked invocations"
328                                    .to_string(),
329                            ));
330                        }
331                        if current.invocation_count > quota.max_invocations {
332                            return Err(BudgetStoreError::Invariant(format!(
333                                "legacy usage for `{}` grant {} exceeds its grant quota",
334                                request.capability_id, request.grant_index
335                            )));
336                        }
337                        state.captured = current.invocation_count;
338                    }
339                    allowed &= state.captured < quota.max_invocations;
340                    quota_before.push(state);
341                }
342            }
343        }
344
345        let cumulative_before = request
346            .cumulative_approval
347            .as_ref()
348            .map(|cumulative| load_or_validate_cumulative(&transaction, cumulative))
349            .transpose()?;
350        let cumulative_state = request
351            .cumulative_approval
352            .as_ref()
353            .zip(cumulative_before.as_ref())
354            .map(|(cumulative, account)| {
355                let prospective = account
356                    .reserved
357                    .checked_add(account.captured)
358                    .and_then(|used| used.checked_add(cumulative.requested_authorized.units))
359                    .ok_or_else(|| {
360                        BudgetStoreError::Overflow(
361                            "cumulative authorized units overflowed u64".to_string(),
362                        )
363                    })?;
364                Ok::<_, BudgetStoreError>(if prospective >= cumulative.effective_threshold.units {
365                    BudgetCumulativeApprovalState::PendingApproval
366                } else {
367                    BudgetCumulativeApprovalState::Authorized
368                })
369            })
370            .transpose()?;
371
372        let event_seq = allocate_budget_replication_seq(&transaction)?;
373        let recorded_at = unix_now();
374        let mut usage_after = current.clone();
375        let mut quota_after = quota_before.clone();
376        let mut cumulative_after = cumulative_before.clone();
377        if allowed {
378            usage_after.invocation_count =
379                usage_after.invocation_count.checked_add(1).ok_or_else(|| {
380                    BudgetStoreError::Overflow("invocation count overflowed u32".to_string())
381                })?;
382            usage_after.total_cost_exposed = usage_after
383                .total_cost_exposed
384                .checked_add(request.requested_exposure_units)
385                .ok_or_else(|| {
386                    BudgetStoreError::Overflow("total cost exposure overflowed u64".to_string())
387                })?;
388            usage_after.updated_at = recorded_at;
389            usage_after.seq = event_seq;
390            write_usage(&transaction, &usage_after)?;
391
392            Self::create_hold(
393                &transaction,
394                hold_id,
395                &request.capability_id,
396                request.grant_index,
397                request.requested_exposure_units,
398                request.authority.as_ref(),
399            )?;
400            write_hold_projection(
401                &transaction,
402                hold_id,
403                admission,
404                quotas.len(),
405                cumulative_state,
406                request.requested_exposure_units,
407            )?;
408            for (quota, state) in quotas.iter().zip(&mut quota_after) {
409                state.reserved = state.reserved.checked_add(1).ok_or_else(|| {
410                    BudgetStoreError::Overflow(
411                        "reserved invocation quota overflowed u32".to_string(),
412                    )
413                })?;
414                state.version = state.version.checked_add(1).ok_or_else(|| {
415                    BudgetStoreError::Overflow(
416                        "invocation quota version overflowed u64".to_string(),
417                    )
418                })?;
419                write_quota_state(&transaction, state)?;
420                write_hold_quota(&transaction, hold_id, quota)?;
421            }
422            write_hold_admission_members(&transaction, hold_id, admission)?;
423            if let (Some(cumulative), Some(state), Some(account)) = (
424                request.cumulative_approval.as_ref(),
425                cumulative_state,
426                cumulative_after.as_mut(),
427            ) {
428                account.reserved = account
429                    .reserved
430                    .checked_add(cumulative.requested_authorized.units)
431                    .ok_or_else(|| {
432                        BudgetStoreError::Overflow(
433                            "reserved cumulative approval overflowed u64".to_string(),
434                        )
435                    })?;
436                account.version = account.version.checked_add(1).ok_or_else(|| {
437                    BudgetStoreError::Overflow(
438                        "cumulative approval version overflowed u64".to_string(),
439                    )
440                })?;
441                write_cumulative_account(&transaction, account)?;
442                write_cumulative_operation(
443                    &transaction,
444                    hold_id,
445                    cumulative,
446                    state,
447                    account.version,
448                )?;
449            }
450        }
451
452        let outcome = match (allowed, cumulative_state) {
453            (false, _) => BudgetAuthorizationOutcome::Denied,
454            (true, Some(BudgetCumulativeApprovalState::PendingApproval)) => {
455                BudgetAuthorizationOutcome::ApprovalRequired
456            }
457            (true, _) => BudgetAuthorizationOutcome::Authorized,
458        };
459        let authorization_kind = if quotas.is_empty() && request.cumulative_approval.is_none() {
460            BudgetMutationKind::AuthorizeExposure
461        } else {
462            BudgetMutationKind::ReserveInvocation
463        };
464        let event = Self::append_mutation_event(
465            &transaction,
466            Some(event_id),
467            Some(hold_id),
468            request.authority.as_ref(),
469            &request.capability_id,
470            request.grant_index,
471            authorization_kind,
472            match outcome {
473                BudgetAuthorizationOutcome::Denied => Some(false),
474                BudgetAuthorizationOutcome::ApprovalRequired => None,
475                BudgetAuthorizationOutcome::Authorized => Some(true),
476            },
477            event_seq,
478            allowed.then_some(usage_after.seq),
479            request.requested_exposure_units,
480            0,
481            request.max_invocations,
482            request.max_cost_per_invocation,
483            request.max_total_cost_units,
484            usage_after.invocation_count,
485            usage_after.total_cost_exposed,
486            usage_after.total_cost_realized_spend,
487        )?;
488        write_authorization_event_projection(
489            &transaction,
490            &event.event_id,
491            admission,
492            outcome,
493            &quotas,
494            &quota_before,
495            &quota_after,
496            request.cumulative_approval.as_ref(),
497            cumulative_state,
498            cumulative_before.as_ref(),
499            cumulative_after.as_ref(),
500            !request.invocation_quotas.is_empty(),
501            request.requested_exposure_units,
502        )?;
503        self.append_joint_commit(&transaction, authorization_kind, event_id, event_seq)?;
504        let decision = authorization_decision(
505            self,
506            request.clone(),
507            outcome,
508            event_seq,
509            event.recorded_at,
510            usage_after,
511            quota_after,
512            cumulative_state,
513            cumulative_after,
514        )?;
515        let operation =
516            self.bind_authorization_to_admission(&transaction, &request, &decision, binding, true)?;
517        self.commit_joint_transaction(transaction)?;
518        self.sync_joint_anchor(&connection)?;
519        Ok((decision, operation))
520    }
521
522    fn bind_authorization_to_admission(
523        &self,
524        transaction: &Transaction<'_>,
525        request: &BudgetAuthorizeHoldRequest,
526        decision: &BudgetAuthorizeHoldDecision,
527        binding: Option<AdmissionAuthorizationBinding<'_>>,
528        insert_journal: bool,
529    ) -> Result<Option<chio_kernel::admission_operation::AdmissionOperationV1>, BudgetStoreError>
530    {
531        let Some(binding) = binding else {
532            return Ok(None);
533        };
534        // A denied authorization reserves nothing, so its operation is untouched. An
535        // approval-required authorization does reserve budget, so its binding is still
536        // validated against the operation before the joint transaction commits.
537        let approval_required =
538            matches!(decision, BudgetAuthorizeHoldDecision::ApprovalRequired(_));
539        if !matches!(decision, BudgetAuthorizeHoldDecision::Authorized(_)) && !approval_required {
540            return Ok(Some(binding.operation.clone()));
541        }
542        let admission = request.admission_binding.as_ref().ok_or_else(|| {
543            BudgetStoreError::Invariant("combined authorization omitted admission binding".into())
544        })?;
545        let hold_id = request.hold_id.as_deref().ok_or_else(|| {
546            BudgetStoreError::Invariant("combined authorization omitted hold_id".into())
547        })?;
548        let event_id = request.event_id.as_deref().ok_or_else(|| {
549            BudgetStoreError::Invariant("combined authorization omitted event_id".into())
550        })?;
551        let operation_id = binding.operation.binding().operation_id().as_str();
552        if admission.operation_id != operation_id
553            || binding.operation.binding().capability_id().as_str() != request.capability_id
554        {
555            return Err(BudgetStoreError::Invariant(
556                "combined authorization does not match its admission operation".to_owned(),
557            ));
558        }
559        let requires_payment = binding
560            .operation
561            .binding()
562            .participant_requirements()
563            .payment;
564        let requires_credit_exposure = binding
565            .operation
566            .binding()
567            .participant_requirements()
568            .credit_exposure;
569        let owner = self.serving_owner.as_deref().ok_or_else(|| {
570            BudgetStoreError::Invariant(
571                "combined authorization requires a serving owner".to_owned(),
572            )
573        })?;
574        if requires_payment != binding.payment_journal.is_some() {
575            return Err(BudgetStoreError::Invariant(
576                "combined authorization payment participant does not match operation requirements"
577                    .to_owned(),
578            ));
579        }
580        if requires_credit_exposure != binding.credit_exposure.is_some() {
581            return Err(BudgetStoreError::Invariant(
582                "combined authorization credit exposure participant does not match operation requirements"
583                    .to_owned(),
584            ));
585        }
586        if approval_required {
587            // Monetary participants attach only once approval clears, and the move to
588            // `ApprovalRequired` carries a kernel-signed threshold proposal this store
589            // cannot mint, so the kernel performs that transition after the reservation
590            // commits. Until it does, the reserved hold stays `authorized` and the
591            // startup reaper reverses it.
592            return Ok(Some(binding.operation.clone()));
593        }
594        if let Some(journal) = binding.payment_journal {
595            let grant_index = u32::try_from(request.grant_index).map_err(|_| {
596                BudgetStoreError::Invariant(
597                    "combined authorization grant index exceeds payment journal range".to_owned(),
598                )
599            })?;
600            if journal.operation_id != operation_id
601                || journal.request_namespace_digest
602                    != binding
603                        .operation
604                        .binding()
605                        .request_namespace_digest()
606                        .as_str()
607                || journal.request_id != binding.operation.binding().request_id().as_str()
608                || journal.capability_id != request.capability_id
609                || journal.grant_index != grant_index
610                || journal.hold_id.as_deref() != Some(hold_id)
611                || journal.amount_units != request.requested_exposure_units
612                || journal.created_at_unix_ms > binding.trusted_now_unix_ms
613            {
614                return Err(BudgetStoreError::Invariant(
615                    "payment journal does not match the combined authorization".to_owned(),
616                ));
617            }
618            if insert_journal {
619                insert_payment_journal(transaction, journal)?;
620                owner
621                    .append_global_commit(
622                        transaction,
623                        "payment_hold_placed",
624                        "payment",
625                        operation_id,
626                        journal.journal_version,
627                    )
628                    .map_err(super::store::map_serving_owner_error)?;
629            } else {
630                let stored = load_payment_journal(transaction, operation_id)?.ok_or_else(|| {
631                    BudgetStoreError::Invariant(
632                        "combined authorization replay lost its payment journal".to_owned(),
633                    )
634                })?;
635                if !stored.matches_hold_replay(journal) {
636                    return Err(BudgetStoreError::Invariant(
637                        "combined authorization replay does not match its payment journal"
638                            .to_owned(),
639                    ));
640                }
641            }
642        }
643        let credit_exposure_reservation = binding
644            .credit_exposure
645            .map(|credit_exposure| {
646                credit_exposure.validate().map_err(|error| {
647                    BudgetStoreError::Invariant(format!(
648                        "credit exposure reservation request is invalid: {error}"
649                    ))
650                })?;
651                let bind = credit_exposure.credit_facility_bind.body();
652                if credit_exposure.operation_id != operation_id
653                    || credit_exposure.request_id
654                        != binding.operation.binding().request_id().as_str()
655                    || credit_exposure.authorities.capability_id()
656                        != binding.operation.binding().capability_id().as_str()
657                    || credit_exposure.amount.units != request.requested_exposure_units
658                {
659                    return Err(BudgetStoreError::Invariant(
660                        "credit exposure reservation does not match the combined authorization"
661                            .to_owned(),
662                    ));
663                }
664                credit_exposure
665                    .authorities
666                    .ensure_current_at(binding.trusted_now_unix_ms / 1_000)
667                    .map_err(|error| {
668                        BudgetStoreError::Invariant(format!(
669                            "credit exposure authority is not current: {error}"
670                        ))
671                    })?;
672                credit_exposure
673                    .credit_facility_bind
674                    .ensure_current_at(binding.trusted_now_unix_ms)
675                    .map_err(|error| {
676                        BudgetStoreError::Invariant(format!(
677                            "credit facility bind is not current: {error}"
678                        ))
679                    })?;
680                let account_version =
681                    bind.expected_exposure_version()
682                        .checked_add(1)
683                        .ok_or_else(|| {
684                            BudgetStoreError::Invariant(
685                                "credit exposure account version overflowed".to_owned(),
686                            )
687                        })?;
688                let resource_fence =
689                    bind.expected_exposure_fence()
690                        .checked_add(1)
691                        .ok_or_else(|| {
692                            BudgetStoreError::Invariant(
693                                "credit exposure resource fence overflowed".to_owned(),
694                            )
695                        })?;
696                chio_credit::obligation::CreditExposureReservationRecordV1::prepare_reserved(
697                    credit_exposure,
698                    account_version,
699                    resource_fence,
700                )
701                .map_err(|error| {
702                    BudgetStoreError::Invariant(format!(
703                        "credit exposure reservation could not be prepared: {error}"
704                    ))
705                })
706            })
707            .transpose()?;
708        if let Some(reservation) = credit_exposure_reservation.as_ref() {
709            if insert_journal {
710                if crate::admission_operation_store::load_credit_exposure_reservation_tx(
711                    transaction,
712                    operation_id,
713                )
714                .map_err(|error| map_credit_exposure_error(error, owner.fence.owner_epoch))?
715                .is_some()
716                {
717                    return Err(BudgetStoreError::Invariant(
718                        "fresh combined authorization found an existing credit exposure reservation"
719                            .to_owned(),
720                    ));
721                }
722            } else {
723                let stored = crate::admission_operation_store::load_credit_exposure_reservation_tx(
724                    transaction,
725                    operation_id,
726                )
727                .map_err(|error| map_credit_exposure_error(error, owner.fence.owner_epoch))?
728                .ok_or_else(|| {
729                    BudgetStoreError::Invariant(
730                        "combined authorization replay lost its credit exposure reservation"
731                            .to_owned(),
732                    )
733                })?;
734                stored.validate().map_err(|error| {
735                    BudgetStoreError::Invariant(format!(
736                        "combined authorization replay has an invalid credit exposure reservation: {error}"
737                    ))
738                })?;
739                let expected_state = match binding.operation.state() {
740                    chio_kernel::admission_operation::AdmissionOperationState::Completed => {
741                        chio_credit::obligation::CreditExposureReservationStateV1::Committed
742                    }
743                    chio_kernel::admission_operation::AdmissionOperationState::CompensatedBeforeDispatch => {
744                        chio_credit::obligation::CreditExposureReservationStateV1::ReleasedBeforeDispatch
745                    }
746                    chio_kernel::admission_operation::AdmissionOperationState::NotAcceptedAfterDispatchCommit
747                    | chio_kernel::admission_operation::AdmissionOperationState::OutcomeUnknownAfterDispatch => {
748                        chio_credit::obligation::CreditExposureReservationStateV1::OutcomeUnknown
749                    }
750                    _ => chio_credit::obligation::CreditExposureReservationStateV1::Reserved,
751                };
752                if stored.reservation_digest() != reservation.reservation_digest()
753                    || stored.state() != expected_state
754                {
755                    return Err(BudgetStoreError::Invariant(
756                        "combined authorization replay conflicts with its credit exposure reservation"
757                            .to_owned(),
758                    ));
759                }
760            }
761        }
762        let commit_index = authorization_commit_index(decision)?;
763        let participant_digest = transaction.query_row(
764            r#"
765            SELECT projection_reference_digest
766            FROM authority_global_commits
767            WHERE projection_kind = 'budget' AND projection_key = ?1
768              AND projection_sequence = ?2
769            "#,
770            params![
771                event_id,
772                budget_u64_to_sqlite(commit_index, "budget authorization sequence")?
773            ],
774            |row| row.get::<_, String>(0),
775        )?;
776        let requirements = binding.operation.binding().participant_requirements();
777        let authorization_source = if requirements.broker_attempt {
778            chio_kernel::admission_operation::AdmissionOperationState::BrokerAttemptRegistered
779        } else {
780            chio_kernel::admission_operation::AdmissionOperationState::Prepared
781        };
782        let operation = if binding.operation.state() == authorization_source {
783            crate::admission_operation_store::advance_budget_authorization_tx(
784                transaction,
785                owner,
786                crate::admission_operation_store::BudgetAuthorizationAdvance {
787                    expected: binding.operation,
788                    recovery_lease: binding.recovery_lease,
789                    hold_id,
790                    payment_required: requires_payment,
791                    credit_exposure_reservation_digest: credit_exposure_reservation
792                        .as_ref()
793                        .map(chio_credit::obligation::CreditExposureReservationRecordV1::reservation_digest),
794                    participant_digest: &participant_digest,
795                    trusted_now_unix_ms: binding.trusted_now_unix_ms,
796                },
797            )
798        } else {
799            crate::admission_operation_store::verify_budget_authorization_replay_tx(
800                transaction,
801                binding.operation,
802                hold_id,
803                requires_payment,
804                credit_exposure_reservation.as_ref().map(
805                    chio_credit::obligation::CreditExposureReservationRecordV1::reservation_digest,
806                ),
807                &participant_digest,
808            )
809        };
810        let operation = operation.map_err(|error| match error {
811            chio_kernel::admission_operation::AdmissionOperationStoreError::Fenced => {
812                BudgetStoreError::Fenced {
813                    expected_epoch: owner.fence.owner_epoch,
814                    actual_epoch: None,
815                }
816            }
817            chio_kernel::admission_operation::AdmissionOperationStoreError::OutcomeUnknown(
818                detail,
819            ) => BudgetStoreError::OutcomeUnknown(detail),
820            error => BudgetStoreError::Invariant(error.to_string()),
821        })?;
822        let expected_credit_digest = credit_exposure_reservation
823            .as_ref()
824            .map(chio_credit::obligation::CreditExposureReservationRecordV1::reservation_digest);
825        if operation
826            .credit_exposure_reservation_digest()
827            .map(chio_kernel::admission_operation::AdmissionDigest::as_str)
828            != expected_credit_digest
829        {
830            return Err(BudgetStoreError::Invariant(
831                "combined authorization lost its credit exposure reservation".to_owned(),
832            ));
833        }
834        if insert_journal {
835            if let Some(reservation) = credit_exposure_reservation.as_ref() {
836                crate::admission_operation_store::reserve_credit_exposure_tx(
837                    transaction,
838                    reservation,
839                    &owner.fence,
840                    binding.trusted_now_unix_ms,
841                )
842                .map_err(|error| map_credit_exposure_error(error, owner.fence.owner_epoch))?;
843            }
844        }
845        let stored_operation = crate::admission_operation_store::load_operation_for_participant_tx(
846            transaction,
847            binding.operation.binding().operation_id(),
848        )
849        .map_err(|error| map_credit_exposure_error(error, owner.fence.owner_epoch))?
850        .ok_or_else(|| {
851            BudgetStoreError::Invariant(
852                "combined authorization lost its admission operation".to_owned(),
853            )
854        })?;
855        if stored_operation != operation {
856            return Err(BudgetStoreError::Invariant(
857                "combined authorization admission operation was not durably advanced".to_owned(),
858            ));
859        }
860        Ok(Some(operation))
861    }
862
863    fn authorization_decision_from_event(
864        &self,
865        transaction: &Transaction<'_>,
866        event: &BudgetMutationRecord,
867    ) -> Result<BudgetAuthorizeHoldDecision, BudgetStoreError> {
868        let hold_id = event.hold_id.as_deref().ok_or_else(|| {
869            BudgetStoreError::Invariant("composite authorization lost hold_id".to_string())
870        })?;
871        if let Some(hold) = load_structured_hold(transaction, hold_id)? {
872            if hold.invocation_state == BudgetInvocationState::Reversed {
873                return Err(BudgetStoreError::Invariant(
874                    "budget authorization replay references a terminally reversed hold".to_string(),
875                ));
876            }
877            if hold.invocation_state == BudgetInvocationState::Captured {
878                return captured_authorization_decision(self, transaction, &hold);
879            }
880        }
881        let latest = latest_hold_event_seq(transaction, hold_id)?.unwrap_or(0);
882        if latest != event.event_seq {
883            return Err(BudgetStoreError::Invariant(format!(
884                "budget hold `{hold_id}` authorization event was superseded"
885            )));
886        }
887        decision_from_persisted_event(self, transaction, event)
888    }
889
890    fn validate_observed_revocation(
891        &self,
892        transaction: &Transaction<'_>,
893        admission: &BudgetAdmissionBinding,
894    ) -> Result<(), BudgetStoreError> {
895        let Some(observation) = admission.last_observed_revocation.as_ref() else {
896            return Ok(());
897        };
898        let owner = self.serving_owner.as_ref().ok_or_else(|| {
899            BudgetStoreError::Invariant(
900                "revocation commit metadata requires a joint sqlite serving owner".to_string(),
901            )
902        })?;
903        let authority_head = transaction.query_row(
904            "SELECT head_index FROM admission_authority_meta WHERE singleton = 1",
905            [],
906            |row| row.get::<_, i64>(0),
907        )?;
908        let authority_head = u64::try_from(authority_head).map_err(|_| {
909            BudgetStoreError::Invariant("negative sqlite authority commit index".to_string())
910        })?;
911        if observation.authority.authority_id != owner.fence.store_uuid
912            || observation.authority.lease_epoch != owner.fence.owner_epoch
913            || observation.authority.lease_id != owner.fence.lease_id
914            || observation.guarantee_level != BudgetGuaranteeLevel::SingleNodeAtomic
915            || observation.commit_index != authority_head
916        {
917            return Err(BudgetStoreError::Invariant(
918                "fresh revocation commit metadata does not match the active sqlite authority head"
919                    .to_string(),
920            ));
921        }
922        Ok(())
923    }
924
925    fn validate_joint_authority(
926        &self,
927        authority: Option<&BudgetEventAuthority>,
928    ) -> Result<(), BudgetStoreError> {
929        let owner = self.serving_owner.as_ref().ok_or_else(|| {
930            BudgetStoreError::Invariant(
931                "structured sqlite mutations require a provisioned serving owner".to_string(),
932            )
933        })?;
934        let expected = BudgetEventAuthority {
935            authority_id: owner.fence.store_uuid.clone(),
936            lease_id: owner.fence.lease_id.clone(),
937            lease_epoch: owner.fence.owner_epoch,
938        };
939        if authority != Some(&expected) {
940            return Err(BudgetStoreError::Invariant(
941                "structured sqlite mutation authority does not match the active serving owner"
942                    .to_string(),
943            ));
944        }
945        Ok(())
946    }
947
948    fn validate_persisted_authority(
949        &self,
950        connection: &Connection,
951        identity: &str,
952        persisted: Option<&BudgetEventAuthority>,
953        requested: Option<&BudgetEventAuthority>,
954    ) -> Result<(), BudgetStoreError> {
955        let Some(owner) = self.serving_owner.as_ref() else {
956            SqliteBudgetStore::validate_hold_authority(identity, persisted, requested)?;
957            return Ok(());
958        };
959        self.validate_joint_authority(requested)?;
960        let Some(persisted) = persisted else {
961            return Err(BudgetStoreError::Invariant(format!(
962                "budget authority record `{identity}` has no serving-owner fence"
963            )));
964        };
965        if persisted.authority_id != owner.fence.store_uuid {
966            return Err(BudgetStoreError::Invariant(format!(
967                "budget authority record `{identity}` is outside the active serving-owner fence"
968            )));
969        }
970        crate::serving_owner::verify_historical_budget_authority(connection, persisted).map_err(
971            |_| {
972                BudgetStoreError::Invariant(format!(
973                    "budget authority record `{identity}` is outside durable serving lease history"
974                ))
975            },
976        )?;
977        Ok(())
978    }
979}
980
981fn authorization_commit_index(
982    decision: &BudgetAuthorizeHoldDecision,
983) -> Result<u64, BudgetStoreError> {
984    let metadata = match decision {
985        BudgetAuthorizeHoldDecision::Authorized(value) => &value.metadata,
986        BudgetAuthorizeHoldDecision::ApprovalRequired(value) => &value.metadata,
987        BudgetAuthorizeHoldDecision::Denied(value) => &value.metadata,
988        BudgetAuthorizeHoldDecision::AlreadyCaptured(_) => {
989            return Err(BudgetStoreError::Invariant(
990                "combined admission authorization was already captured".to_owned(),
991            ));
992        }
993    };
994    metadata.budget_commit_index.ok_or_else(|| {
995        BudgetStoreError::Invariant(
996            "combined admission authorization omitted its durable sequence".to_owned(),
997        )
998    })
999}
1000
1001fn map_credit_exposure_error(
1002    error: chio_kernel::admission_operation::AdmissionOperationStoreError,
1003    expected_epoch: u64,
1004) -> BudgetStoreError {
1005    match error {
1006        chio_kernel::admission_operation::AdmissionOperationStoreError::Fenced => {
1007            BudgetStoreError::Fenced {
1008                expected_epoch,
1009                actual_epoch: None,
1010            }
1011        }
1012        chio_kernel::admission_operation::AdmissionOperationStoreError::OutcomeUnknown(detail) => {
1013            BudgetStoreError::OutcomeUnknown(detail)
1014        }
1015        error => BudgetStoreError::Invariant(error.to_string()),
1016    }
1017}