Skip to main content

chio_store_sqlite/
channel_release_publisher_store.rs

1use std::sync::{Arc, Mutex, MutexGuard};
2
3use chio_core::canonical::canonical_json_bytes;
4use chio_core::economic_continuity::{
5    economic_effect_slot_from_head, EconomicAdmissionHandoffStateV1, EconomicContentV1,
6    EconomicEffectSlotV1, EconomicEffectStateV1, EconomicEffectTerminalV1, EconomicResourceHeadV1,
7    EconomicResourceKeyV1, VerifiedEconomicEffectDispatch,
8};
9use chio_core::{sha256_hex, StoreMutationFence};
10use chio_kernel::admission_operation::{
11    AdmissionOperationId, AdmissionOperationKind, AdmissionOperationState, AdmissionOperationV1,
12    SideEffectClass,
13};
14use chio_settle::channel::{
15    ChannelEscrowReservationStatusV1, ChannelEscrowReservationViewV1, ChannelLifecycleStatusV1,
16    ChannelLifecycleViewV1, VerifiedChannelLifecycleSnapshotV1,
17    VerifiedSignedChannelReleaseAuthorizationV1, CHANNEL_RELEASE_BROADCAST_EFFECT_KIND,
18    CHANNEL_RELEASE_ROOT_PUBLICATION_EFFECT_KIND,
19};
20use chio_settle::{PreparedAuthorizedChannelMerkleReleaseV1, SettlementChainConfig};
21use rusqlite::{
22    params, Connection, ErrorCode, OptionalExtension, Row, Transaction, TransactionBehavior,
23};
24use serde::{Deserialize, Serialize};
25
26use crate::serving_owner::{SqliteServingOwner, SqliteServingOwnerError};
27
28mod persistence;
29mod qualification;
30
31use persistence::*;
32pub(crate) use persistence::{
33    initialize_channel_release_publisher_schema, verify_channel_release_publisher_invariants,
34};
35use qualification::*;
36
37const CHANNEL_RELEASE_PUBLISHER_SCHEMA_KEY: &str = "channel_release_publisher";
38pub(crate) const CHANNEL_RELEASE_PUBLISHER_SUPPORTED_SCHEMA_VERSION: i32 = 1;
39const CHANNEL_RELEASE_PUBLISHER_SCHEMA_ANCHORS: &[&str] = &[
40    "chio_channel_release_publisher_meta",
41    "chio_channel_release_publications",
42    "channel_lifecycle_records",
43];
44const CHANNEL_RELEASE_PUBLISHER_SCHEMA: &str = include_str!("channel_release_publisher_store.sql");
45const CHANNEL_RELEASE_AUTHORIZATION_DIGEST_DOMAIN: &[u8] =
46    b"chio.channel.release-authorization.signed-digest.v1\0";
47const MAX_PUBLISHER_ARTIFACT_BYTES: usize = 1024 * 1024;
48#[cfg(test)]
49const MAX_FAILURE_DETAIL_BYTES: usize = 4 * 1024;
50const CHANNEL_ROOT_PUBLICATION_RESULT_SCHEMA: &str = "chio.channel.root-publication-result.v1";
51
52#[derive(Debug, Deserialize)]
53#[serde(rename_all = "camelCase", deny_unknown_fields)]
54struct ChannelRootPublicationResultV1 {
55    schema: String,
56    publication_root: String,
57    call_digest: String,
58    transaction_hash: String,
59}
60
61#[derive(Debug, thiserror::Error)]
62pub enum ChannelReleasePublisherError {
63    #[error("channel release publisher store is unavailable: {0}")]
64    Unavailable(String),
65    #[error("channel release publisher mutation was fenced")]
66    Fenced,
67    #[error("channel release publisher record conflicts with retained state")]
68    Conflict,
69    #[error("channel release publisher invariant failed: {0}")]
70    Invalid(String),
71    #[error("channel release publisher durable outcome is unknown: {0}")]
72    OutcomeUnknown(String),
73    #[error("channel release publisher broadcast is disabled: {0}")]
74    BroadcastDisabled(String),
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum ChannelReleasePublicationStatusV1 {
79    DispatchCommitted,
80    Submitted,
81    Unknown,
82    Incident,
83}
84
85impl ChannelReleasePublicationStatusV1 {
86    fn parse(value: &str) -> Result<Self, ChannelReleasePublisherError> {
87        match value {
88            "dispatch_committed" => Ok(Self::DispatchCommitted),
89            "submitted" => Ok(Self::Submitted),
90            "unknown" => Ok(Self::Unknown),
91            "incident" => Ok(Self::Incident),
92            _ => Err(invalid("retained channel release status is invalid")),
93        }
94    }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct ChannelReleasePublicationRecordV1 {
99    channel_id: String,
100    publication_binding_digest: String,
101    authorization_digest: String,
102    prepared_call_digest: String,
103    publisher_fence: u64,
104    status: ChannelReleasePublicationStatusV1,
105    transaction_hash: Option<String>,
106    failure_detail: Option<String>,
107    record_version: u64,
108    created_at_unix_ms: u64,
109    updated_at_unix_ms: u64,
110}
111
112impl ChannelReleasePublicationRecordV1 {
113    #[must_use]
114    pub fn channel_id(&self) -> &str {
115        &self.channel_id
116    }
117
118    #[must_use]
119    pub fn publication_binding_digest(&self) -> &str {
120        &self.publication_binding_digest
121    }
122
123    #[must_use]
124    pub fn authorization_digest(&self) -> &str {
125        &self.authorization_digest
126    }
127
128    #[must_use]
129    pub fn prepared_call_digest(&self) -> &str {
130        &self.prepared_call_digest
131    }
132
133    #[must_use]
134    pub const fn publisher_fence(&self) -> u64 {
135        self.publisher_fence
136    }
137
138    #[must_use]
139    pub const fn status(&self) -> ChannelReleasePublicationStatusV1 {
140        self.status
141    }
142
143    #[must_use]
144    pub fn transaction_hash(&self) -> Option<&str> {
145        self.transaction_hash.as_deref()
146    }
147
148    #[must_use]
149    pub fn failure_detail(&self) -> Option<&str> {
150        self.failure_detail.as_deref()
151    }
152
153    #[must_use]
154    pub const fn record_version(&self) -> u64 {
155        self.record_version
156    }
157
158    #[must_use]
159    pub const fn created_at_unix_ms(&self) -> u64 {
160        self.created_at_unix_ms
161    }
162
163    #[must_use]
164    pub const fn updated_at_unix_ms(&self) -> u64 {
165        self.updated_at_unix_ms
166    }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub enum ChannelReleaseSubmissionOutcomeV1 {
171    Submitted(ChannelReleasePublicationRecordV1),
172    AlreadyClaimed(ChannelReleasePublicationRecordV1),
173}
174
175#[derive(Debug)]
176pub struct VerifiedChannelReleasePublicationV1 {
177    candidate: ChannelReleasePublisherCandidate,
178}
179
180impl VerifiedChannelReleasePublicationV1 {
181    pub fn verify(
182        authorization: &VerifiedSignedChannelReleaseAuthorizationV1,
183        prepared: &PreparedAuthorizedChannelMerkleReleaseV1,
184        closing: &VerifiedChannelLifecycleSnapshotV1,
185    ) -> Result<Self, ChannelReleasePublisherError> {
186        Ok(Self {
187            candidate: ChannelReleasePublisherCandidate::from_verified(
188                authorization,
189                prepared,
190                closing,
191            )?,
192        })
193    }
194}
195
196#[cfg(test)]
197#[derive(Debug, Clone)]
198struct ChannelReleaseBroadcastPermitV1 {
199    channel_id: String,
200    publication_binding_digest: String,
201    authorization_digest: String,
202    prepared_call_digest: String,
203    publisher_fence: u64,
204    record_version: u64,
205}
206
207#[cfg(test)]
208#[derive(Debug, Clone)]
209enum ChannelReleasePermitClaimV1 {
210    Claimed {
211        permit: ChannelReleaseBroadcastPermitV1,
212    },
213    ExactReplay(ChannelReleasePublicationRecordV1),
214}
215
216#[derive(Debug, Clone, Serialize)]
217#[serde(rename_all = "camelCase")]
218struct ChannelReleasePublisherCandidate {
219    channel_id: String,
220    chain_id: String,
221    escrow_contract: String,
222    escrow_id: String,
223    open_digest: String,
224    close_digest: String,
225    close_body_digest: String,
226    effective_close_digest: String,
227    final_state_digest: String,
228    final_state_sequence: u64,
229    source_channel_state_version: u64,
230    source_escrow_reservation_version: u64,
231    source_lifecycle_fence: u64,
232    source_checkpoint_sequence: u64,
233    source_checkpoint_digest: String,
234    source_channel_head_digest: String,
235    source_escrow_head_digest: String,
236    closing_checkpoint_sequence: u64,
237    closing_checkpoint_digest: String,
238    closing_channel_head_digest: String,
239    closing_escrow_head_digest: String,
240    closing_channel_predecessor_digest: String,
241    closing_escrow_predecessor_digest: String,
242    closing_lifecycle: ChannelLifecycleViewV1,
243    closing_escrow: ChannelEscrowReservationViewV1,
244    bound_token_base_units: String,
245    release_token_base_units: String,
246    refund_token_base_units: String,
247    original_operator: String,
248    original_operator_key_hash: String,
249    beneficiary_address: String,
250    asset_binding_digest: String,
251    original_dispatch_digest: String,
252    close_submission_cutoff_unix_ms: u64,
253    escrow_deadline_unix_ms: u64,
254    publisher_fence: u64,
255    authorized_at_unix_ms: u64,
256    frost_slot_id: String,
257    frost_authorization_id: String,
258    frost_action_digest: String,
259    frost_envelope_digest: String,
260    frost_scope_id: String,
261    frost_resource_id: String,
262    frost_resource_version: u64,
263    frost_resource_fence: u64,
264    frost_roster_digest: String,
265    frost_key_epoch: u64,
266    frost_issued_at_unix_ms: u64,
267    publication_root: String,
268    root_operation_id: String,
269    root_effect_slot_id: String,
270    root_effect_scope_id: String,
271    root_effect_kind: String,
272    root_idempotency_key: String,
273    root_call_digest: String,
274    root_resource_head_digest: String,
275    release_operation_id: String,
276    release_effect_slot_id: String,
277    release_effect_scope_id: String,
278    release_effect_kind: String,
279    release_idempotency_key: String,
280    release_call_digest: String,
281    release_resource_head_digest: String,
282    authorization_digest: String,
283    authorization_json: Vec<u8>,
284    prepared_call_digest: String,
285    prepared_call_json: Vec<u8>,
286}
287
288impl ChannelReleasePublisherCandidate {
289    fn from_verified(
290        authorization: &VerifiedSignedChannelReleaseAuthorizationV1,
291        prepared: &PreparedAuthorizedChannelMerkleReleaseV1,
292        closing: &VerifiedChannelLifecycleSnapshotV1,
293    ) -> Result<Self, ChannelReleasePublisherError> {
294        let authority = authorization.authority();
295        let binding = authority.binding();
296        let body = authorization.body();
297        if prepared.authorization() != binding
298            || prepared.authorization_digest() != authority.authorization_digest()
299            || body.authority() != binding
300            || body.publication_root() != prepared.publication_root()
301        {
302            return Err(invalid(
303                "prepared channel release lost its verified authorization",
304            ));
305        }
306        let root = body.root_publication();
307        let release_binding = body.release_broadcast();
308        let source = authority.close().snapshot();
309        let authorization_json = encode(
310            authorization.artifact(),
311            "signed channel release authorization",
312        )?;
313        let authorization_digest = authorization_digest(&authorization_json);
314        if authorization_digest != authorization.digest() {
315            return Err(invalid(
316                "channel release authorization digest is not canonical",
317            ));
318        }
319        let prepared_call_json = prepared
320            .canonical_call()
321            .map_err(|error| invalid(error.to_string()))?;
322        let prepared_call_digest = sha256_hex(&prepared_call_json);
323        if release_binding.call_digest != prepared_call_digest {
324            return Err(invalid(
325                "signed channel release authorization changed the prepared release",
326            ));
327        }
328        let frost = binding.frost();
329        Ok(Self {
330            channel_id: binding.channel_id().to_owned(),
331            chain_id: binding.escrow_reference().chain_id.clone(),
332            escrow_contract: binding.escrow_reference().escrow_contract.clone(),
333            escrow_id: binding.escrow_reference().escrow_id.clone(),
334            open_digest: binding.open_digest().to_owned(),
335            close_digest: binding.close_digest().to_owned(),
336            close_body_digest: binding.close_body_digest().to_owned(),
337            effective_close_digest: binding.effective_close_digest().to_owned(),
338            final_state_digest: binding.final_state_digest().to_owned(),
339            final_state_sequence: binding.final_state_sequence(),
340            source_channel_state_version: binding.channel_state_version(),
341            source_escrow_reservation_version: binding.escrow_reservation_version(),
342            source_lifecycle_fence: binding.lifecycle_fence(),
343            source_checkpoint_sequence: source.checkpoint_sequence(),
344            source_checkpoint_digest: source.checkpoint_digest().to_owned(),
345            source_channel_head_digest: source.channel_head_digest().to_owned(),
346            source_escrow_head_digest: source.escrow_head_digest().to_owned(),
347            closing_checkpoint_sequence: closing.checkpoint_sequence(),
348            closing_checkpoint_digest: closing.checkpoint_digest().to_owned(),
349            closing_channel_head_digest: closing.channel_head_digest().to_owned(),
350            closing_escrow_head_digest: closing.escrow_head_digest().to_owned(),
351            closing_channel_predecessor_digest: closing
352                .channel_predecessor_digest()
353                .ok_or_else(|| invalid("closing channel head has no predecessor"))?
354                .to_owned(),
355            closing_escrow_predecessor_digest: closing
356                .escrow_predecessor_digest()
357                .ok_or_else(|| invalid("closing escrow head has no predecessor"))?
358                .to_owned(),
359            closing_lifecycle: closing.lifecycle().clone(),
360            closing_escrow: closing.escrow().clone(),
361            bound_token_base_units: binding.bound_token_base_units().to_owned(),
362            release_token_base_units: binding.expected_release_token_base_units().to_owned(),
363            refund_token_base_units: binding.expected_refund_token_base_units().to_owned(),
364            original_operator: binding.original_operator().to_owned(),
365            original_operator_key_hash: binding.original_operator_key_hash().to_owned(),
366            beneficiary_address: binding.payee_beneficiary_address().to_owned(),
367            asset_binding_digest: binding.asset_binding_digest().to_owned(),
368            original_dispatch_digest: binding.original_web3_dispatch_digest().to_owned(),
369            close_submission_cutoff_unix_ms: binding.close_submission_cutoff_unix_ms(),
370            escrow_deadline_unix_ms: binding.escrow_deadline_unix_ms(),
371            publisher_fence: binding.publisher_fence(),
372            authorized_at_unix_ms: binding.authorized_at_unix_ms(),
373            frost_slot_id: frost.authorization_slot_id.clone(),
374            frost_authorization_id: frost.authorization_id.clone(),
375            frost_action_digest: frost.action_digest.clone(),
376            frost_envelope_digest: frost.signed_envelope_digest.clone(),
377            frost_scope_id: binding.frost_scope_id().to_owned(),
378            frost_resource_id: binding.frost_resource_id().to_owned(),
379            frost_resource_version: binding.frost_resource_version(),
380            frost_resource_fence: binding.frost_resource_fence(),
381            frost_roster_digest: binding.frost_roster_digest().to_owned(),
382            frost_key_epoch: binding.frost_key_epoch(),
383            frost_issued_at_unix_ms: binding.frost_issued_at_unix_ms(),
384            publication_root: body.publication_root().to_owned(),
385            root_operation_id: root.operation_id.clone(),
386            root_effect_slot_id: root.effect_slot_id.clone(),
387            root_effect_scope_id: root.scope_id.clone(),
388            root_effect_kind: root.effect_kind.clone(),
389            root_idempotency_key: root.idempotency_key.clone(),
390            root_call_digest: root.call_digest.clone(),
391            root_resource_head_digest: root.resource_head_digest.clone(),
392            release_operation_id: release_binding.operation_id.clone(),
393            release_effect_slot_id: release_binding.effect_slot_id.clone(),
394            release_effect_scope_id: release_binding.scope_id.clone(),
395            release_effect_kind: release_binding.effect_kind.clone(),
396            release_idempotency_key: release_binding.idempotency_key.clone(),
397            release_call_digest: release_binding.call_digest.clone(),
398            release_resource_head_digest: release_binding.resource_head_digest.clone(),
399            authorization_digest,
400            authorization_json,
401            prepared_call_digest,
402            prepared_call_json,
403        })
404    }
405
406    fn binding_digest(&self) -> Result<String, ChannelReleasePublisherError> {
407        Ok(sha256_hex(&encode(self, "channel release publication")?))
408    }
409
410    fn verify_dispatch(
411        &self,
412        dispatch: &VerifiedEconomicEffectDispatch,
413    ) -> Result<(), ChannelReleasePublisherError> {
414        self.verify_dispatch_slot(dispatch.slot())
415    }
416
417    fn verify_dispatch_slot(
418        &self,
419        slot: &EconomicEffectSlotV1,
420    ) -> Result<(), ChannelReleasePublisherError> {
421        let frost = slot.frost.as_ref();
422        if slot.slot_id != self.release_effect_slot_id
423            || slot.operation_id != self.release_operation_id
424            || slot.resource_key.resource_family != "channel_escrow_reservation"
425            || slot.resource_key.scope_id != self.release_effect_scope_id
426            || slot.resource_key.scope_id != self.frost_scope_id
427            || slot.resource_key.resource_id != self.channel_id
428            || slot.effect_kind != self.release_effect_kind
429            || slot.effect_kind != CHANNEL_RELEASE_BROADCAST_EFFECT_KIND
430            || slot.idempotency_key != self.release_idempotency_key
431            || slot.parameters_digest != self.release_call_digest
432            || slot.parameters_digest != self.prepared_call_digest
433            || slot.resource_head_digest != self.release_resource_head_digest
434            || slot.action_digest != self.frost_action_digest
435            || slot.admission_handoff.state != EconomicAdmissionHandoffStateV1::MutationSubmitted
436            || slot.state != EconomicEffectStateV1::DispatchCommitted
437            || frost.is_none_or(|binding| {
438                binding.authorization_slot_id != self.frost_slot_id
439                    || binding.authorization_id != self.frost_authorization_id
440                    || binding.action_digest != self.frost_action_digest
441                    || binding.signed_envelope_digest != self.frost_envelope_digest
442            })
443        {
444            return Err(ChannelReleasePublisherError::Fenced);
445        }
446        Ok(())
447    }
448}
449
450#[derive(Clone)]
451pub struct SqliteChannelReleasePublisherStore {
452    connection: Arc<Mutex<Connection>>,
453    serving_owner: Arc<SqliteServingOwner>,
454}
455
456impl SqliteChannelReleasePublisherStore {
457    pub(crate) fn open_alongside(
458        connection: Arc<Mutex<Connection>>,
459        serving_owner: Arc<SqliteServingOwner>,
460    ) -> Self {
461        Self {
462            connection,
463            serving_owner,
464        }
465    }
466
467    #[must_use]
468    pub fn mutation_fence(&self) -> StoreMutationFence {
469        self.serving_owner.fence.clone()
470    }
471
472    pub fn verify_invariants(&self) -> Result<(), ChannelReleasePublisherError> {
473        let connection = self.connection()?;
474        verify_channel_release_publisher_invariants(&connection).map_err(owner_error)
475    }
476
477    pub fn load(
478        &self,
479        channel_id: &str,
480    ) -> Result<Option<ChannelReleasePublicationRecordV1>, ChannelReleasePublisherError> {
481        let connection = self.connection()?;
482        self.serving_owner
483            .verify_authority_anchor(&connection)
484            .map_err(owner_error)?;
485        load_record(&connection, channel_id)
486    }
487
488    pub async fn submit_authorized_channel_release(
489        &self,
490        config: &SettlementChainConfig,
491        publication: &VerifiedChannelReleasePublicationV1,
492        dispatch: VerifiedEconomicEffectDispatch,
493        fence: &StoreMutationFence,
494        trusted_now_unix_ms: u64,
495    ) -> Result<ChannelReleaseSubmissionOutcomeV1, ChannelReleasePublisherError> {
496        let candidate = &publication.candidate;
497        if let Some(record) = self.replay_candidate(candidate, fence, trusted_now_unix_ms)? {
498            return Ok(ChannelReleaseSubmissionOutcomeV1::AlreadyClaimed(record));
499        }
500        candidate.verify_dispatch(&dispatch)?;
501        if config.chain_id != candidate.chain_id
502            || config.escrow_contract != candidate.escrow_contract
503            || config.operator_address != candidate.original_operator
504        {
505            return Err(invalid(
506                "settlement configuration does not match channel release authority",
507            ));
508        }
509        config
510            .validate()
511            .map_err(|error| invalid(error.to_string()))?;
512        self.qualify_new_dispatch(candidate, &dispatch, fence, trusted_now_unix_ms)?;
513        Err(ChannelReleasePublisherError::BroadcastDisabled(
514            "deterministic raw transaction signing, nonce leasing, recovery, and live FROST/operator key resolution are unavailable".to_owned(),
515        ))
516    }
517
518    pub fn replay_authorized_channel_release(
519        &self,
520        publication: &VerifiedChannelReleasePublicationV1,
521        fence: &StoreMutationFence,
522        trusted_now_unix_ms: u64,
523    ) -> Result<Option<ChannelReleaseSubmissionOutcomeV1>, ChannelReleasePublisherError> {
524        let candidate = &publication.candidate;
525        self.replay_candidate(candidate, fence, trusted_now_unix_ms)
526            .map(|record| record.map(ChannelReleaseSubmissionOutcomeV1::AlreadyClaimed))
527    }
528
529    fn replay_candidate(
530        &self,
531        candidate: &ChannelReleasePublisherCandidate,
532        fence: &StoreMutationFence,
533        trusted_now_unix_ms: u64,
534    ) -> Result<Option<ChannelReleasePublicationRecordV1>, ChannelReleasePublisherError> {
535        self.replay_candidate_inner(candidate, fence, trusted_now_unix_ms, true)
536    }
537
538    #[cfg(test)]
539    fn replay_candidate_unqualified_for_test(
540        &self,
541        candidate: &ChannelReleasePublisherCandidate,
542        fence: &StoreMutationFence,
543        trusted_now_unix_ms: u64,
544    ) -> Result<Option<ChannelReleasePublicationRecordV1>, ChannelReleasePublisherError> {
545        self.replay_candidate_inner(candidate, fence, trusted_now_unix_ms, false)
546    }
547
548    fn replay_candidate_inner(
549        &self,
550        candidate: &ChannelReleasePublisherCandidate,
551        fence: &StoreMutationFence,
552        _trusted_now_unix_ms: u64,
553        _require_durable_execution: bool,
554    ) -> Result<Option<ChannelReleasePublicationRecordV1>, ChannelReleasePublisherError> {
555        if fence != &self.serving_owner.fence {
556            return Err(ChannelReleasePublisherError::Fenced);
557        }
558        let mut connection = self.connection()?;
559        self.serving_owner
560            .verify_authority_anchor(&connection)
561            .map_err(owner_error)?;
562        let transaction = connection.transaction().map_err(sqlite_error)?;
563        validate_candidate(candidate)?;
564        let binding_digest = candidate.binding_digest()?;
565        match load_record(&transaction, &candidate.channel_id)? {
566            Some(record) if record_matches_candidate(&record, candidate, &binding_digest) => {
567                Ok(Some(record))
568            }
569            Some(_) => Err(ChannelReleasePublisherError::Conflict),
570            None => Ok(None),
571        }
572    }
573
574    #[cfg(test)]
575    fn claim_candidate_unqualified_for_test(
576        &self,
577        candidate: &ChannelReleasePublisherCandidate,
578        fence: &StoreMutationFence,
579        trusted_now_unix_ms: u64,
580    ) -> Result<ChannelReleasePermitClaimV1, ChannelReleasePublisherError> {
581        self.claim_candidate_inner(candidate, fence, trusted_now_unix_ms, false)
582    }
583
584    #[cfg(test)]
585    fn claim_candidate_inner(
586        &self,
587        candidate: &ChannelReleasePublisherCandidate,
588        fence: &StoreMutationFence,
589        trusted_now_unix_ms: u64,
590        require_durable_execution: bool,
591    ) -> Result<ChannelReleasePermitClaimV1, ChannelReleasePublisherError> {
592        if fence != &self.serving_owner.fence {
593            return Err(ChannelReleasePublisherError::Fenced);
594        }
595        let mut connection = self.connection()?;
596        self.serving_owner
597            .verify_authority_anchor(&connection)
598            .map_err(owner_error)?;
599        let transaction = connection
600            .transaction_with_behavior(TransactionBehavior::Immediate)
601            .map_err(sqlite_error)?;
602        validate_candidate(candidate)?;
603        let binding_digest = candidate.binding_digest()?;
604        if let Some(existing) = load_record(&transaction, &candidate.channel_id)? {
605            if record_matches_candidate(&existing, candidate, &binding_digest) {
606                return Ok(ChannelReleasePermitClaimV1::ExactReplay(existing));
607            }
608            return Err(ChannelReleasePublisherError::Conflict);
609        }
610        verify_trusted_time(&transaction, trusted_now_unix_ms)?;
611        validate_candidate_freshness(candidate, trusted_now_unix_ms)?;
612        let lifecycle_json = encode(&candidate.closing_lifecycle, "closing channel lifecycle")?;
613        let escrow_json = encode(&candidate.closing_escrow, "closing escrow reservation")?;
614        qualify_durable_closing(&transaction, candidate, &lifecycle_json, &escrow_json)?;
615        if require_durable_execution {
616            let _ = qualify_durable_release_execution(&transaction, candidate, fence)?;
617        }
618        advance_trusted_time(&transaction, trusted_now_unix_ms)?;
619        insert_publication(
620            &transaction,
621            candidate,
622            &binding_digest,
623            &lifecycle_json,
624            &escrow_json,
625            fence,
626            trusted_now_unix_ms,
627        )?;
628        self.serving_owner
629            .append_global_commit(
630                &transaction,
631                "channel_release_dispatch_committed",
632                "channel_release_publication",
633                &candidate.channel_id,
634                1,
635            )
636            .map_err(owner_error)?;
637        transaction.commit().map_err(|error| {
638            owner_error(self.serving_owner.outcome_unknown(format!(
639                "sqlite channel release claim outcome is unknown: {error}"
640            )))
641        })?;
642        self.serving_owner
643            .sync_authority_anchor(&connection)
644            .map_err(owner_error)?;
645        Ok(ChannelReleasePermitClaimV1::Claimed {
646            permit: ChannelReleaseBroadcastPermitV1 {
647                channel_id: candidate.channel_id.clone(),
648                publication_binding_digest: binding_digest,
649                authorization_digest: candidate.authorization_digest.clone(),
650                prepared_call_digest: candidate.prepared_call_digest.clone(),
651                publisher_fence: candidate.publisher_fence,
652                record_version: 1,
653            },
654        })
655    }
656
657    fn qualify_new_dispatch(
658        &self,
659        candidate: &ChannelReleasePublisherCandidate,
660        dispatch: &VerifiedEconomicEffectDispatch,
661        fence: &StoreMutationFence,
662        trusted_now_unix_ms: u64,
663    ) -> Result<(), ChannelReleasePublisherError> {
664        if fence != &self.serving_owner.fence {
665            return Err(ChannelReleasePublisherError::Fenced);
666        }
667        let mut connection = self.connection()?;
668        self.serving_owner
669            .verify_authority_anchor(&connection)
670            .map_err(owner_error)?;
671        let transaction = connection.transaction().map_err(sqlite_error)?;
672        verify_trusted_time(&transaction, trusted_now_unix_ms)?;
673        validate_candidate(candidate)?;
674        validate_candidate_freshness(candidate, trusted_now_unix_ms)?;
675        let lifecycle_json = encode(&candidate.closing_lifecycle, "closing channel lifecycle")?;
676        let escrow_json = encode(&candidate.closing_escrow, "closing escrow reservation")?;
677        qualify_durable_closing(&transaction, candidate, &lifecycle_json, &escrow_json)?;
678        let durable_slot = qualify_durable_release_execution(&transaction, candidate, fence)?;
679        qualify_exact_dispatch_slot(
680            candidate,
681            &durable_slot,
682            dispatch.slot(),
683            dispatch.commit_id(),
684        )
685    }
686
687    #[cfg(test)]
688    fn record_submission_unknown(
689        &self,
690        permit: &ChannelReleaseBroadcastPermitV1,
691        detail: &str,
692        fence: &StoreMutationFence,
693        trusted_now_unix_ms: u64,
694    ) -> Result<ChannelReleasePublicationRecordV1, ChannelReleasePublisherError> {
695        self.record_submission(
696            permit,
697            SubmissionRecord::Unknown(detail),
698            fence,
699            trusted_now_unix_ms,
700        )
701    }
702
703    #[cfg(test)]
704    fn record_submission(
705        &self,
706        permit: &ChannelReleaseBroadcastPermitV1,
707        submission: SubmissionRecord<'_>,
708        fence: &StoreMutationFence,
709        trusted_now_unix_ms: u64,
710    ) -> Result<ChannelReleasePublicationRecordV1, ChannelReleasePublisherError> {
711        if fence != &self.serving_owner.fence || trusted_now_unix_ms == 0 {
712            return Err(ChannelReleasePublisherError::Fenced);
713        }
714        let mut connection = self.connection()?;
715        self.serving_owner
716            .verify_authority_anchor(&connection)
717            .map_err(owner_error)?;
718        let transaction = connection
719            .transaction_with_behavior(TransactionBehavior::Immediate)
720            .map_err(sqlite_error)?;
721        verify_trusted_time(&transaction, trusted_now_unix_ms)?;
722        let (status, transaction_hash, failure_detail, mutation_kind) = match submission {
723            SubmissionRecord::Submitted(transaction_hash)
724                if is_evm_transaction_hash(transaction_hash) =>
725            {
726                (
727                    "submitted",
728                    Some(transaction_hash),
729                    None,
730                    "channel_release_submitted",
731                )
732            }
733            SubmissionRecord::Unknown(detail)
734                if !detail.is_empty() && detail.len() <= MAX_FAILURE_DETAIL_BYTES =>
735            {
736                ("unknown", None, Some(detail), "channel_release_unknown")
737            }
738            _ => return Err(invalid("channel release submission result is empty")),
739        };
740        let changed = transaction
741            .execute(
742                r#"
743                UPDATE chio_channel_release_publications
744                SET status = ?1, transaction_hash = ?2, failure_detail = ?3,
745                    record_version = record_version + 1,
746                    store_uuid = ?4, store_lease_id = ?5, store_owner_epoch = ?6,
747                    updated_at_unix_ms = ?7
748                WHERE channel_id = ?8 AND status = 'dispatch_committed'
749                  AND publication_binding_digest = ?9
750                  AND authorization_digest = ?10 AND prepared_call_digest = ?11
751                  AND publisher_fence = ?12 AND record_version = ?13
752                  AND updated_at_unix_ms <= ?7
753                "#,
754                params![
755                    status,
756                    transaction_hash,
757                    failure_detail,
758                    &fence.store_uuid,
759                    &fence.lease_id,
760                    sqlite_i64(fence.owner_epoch, "store_owner_epoch")?,
761                    sqlite_i64(trusted_now_unix_ms, "updated_at_unix_ms")?,
762                    &permit.channel_id,
763                    &permit.publication_binding_digest,
764                    &permit.authorization_digest,
765                    &permit.prepared_call_digest,
766                    sqlite_i64(permit.publisher_fence, "publisher_fence")?,
767                    sqlite_i64(permit.record_version, "record_version")?,
768                ],
769            )
770            .map_err(sqlite_error)?;
771        if changed != 1 {
772            let existing = load_record(&transaction, &permit.channel_id)?
773                .ok_or(ChannelReleasePublisherError::Fenced)?;
774            let exact = existing.publication_binding_digest == permit.publication_binding_digest
775                && existing.authorization_digest == permit.authorization_digest
776                && existing.prepared_call_digest == permit.prepared_call_digest
777                && existing.publisher_fence == permit.publisher_fence
778                && existing.status == submission.status()
779                && existing.transaction_hash.as_deref() == submission.transaction_hash()
780                && existing.failure_detail.as_deref() == submission.failure_detail();
781            if exact {
782                return Ok(existing);
783            }
784            return Err(ChannelReleasePublisherError::Fenced);
785        }
786        advance_trusted_time(&transaction, trusted_now_unix_ms)?;
787        self.serving_owner
788            .append_global_commit(
789                &transaction,
790                mutation_kind,
791                "channel_release_publication",
792                &permit.channel_id,
793                permit
794                    .record_version
795                    .checked_add(1)
796                    .ok_or_else(|| invalid("channel release record version overflowed"))?,
797            )
798            .map_err(owner_error)?;
799        transaction.commit().map_err(|error| {
800            owner_error(self.serving_owner.outcome_unknown(format!(
801                "sqlite channel release submission record outcome is unknown: {error}"
802            )))
803        })?;
804        self.serving_owner
805            .sync_authority_anchor(&connection)
806            .map_err(owner_error)?;
807        load_record(&connection, &permit.channel_id)?
808            .ok_or_else(|| invalid("committed channel release record disappeared"))
809    }
810
811    fn connection(&self) -> Result<MutexGuard<'_, Connection>, ChannelReleasePublisherError> {
812        self.connection.lock().map_err(|_| {
813            ChannelReleasePublisherError::Unavailable(
814                "sqlite channel release publisher lock poisoned".to_owned(),
815            )
816        })
817    }
818}
819
820pub(crate) fn quarantine_incomplete_dispatches_at_startup(
821    connection: &mut Connection,
822    serving_owner: &SqliteServingOwner,
823) -> Result<(), SqliteServingOwnerError> {
824    serving_owner.verify_authority_anchor(connection)?;
825    let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?;
826    let retained = {
827        let mut statement = transaction.prepare(
828            r#"
829            SELECT channel_id, record_version, updated_at_unix_ms
830            FROM chio_channel_release_publications
831            WHERE status = 'dispatch_committed'
832            ORDER BY channel_id
833            "#,
834        )?;
835        let rows = statement
836            .query_map([], |row| {
837                Ok((
838                    row.get::<_, String>(0)?,
839                    row.get::<_, i64>(1)?,
840                    row.get::<_, i64>(2)?,
841                ))
842            })?
843            .collect::<Result<Vec<_>, _>>()?;
844        rows
845    };
846    if retained.is_empty() {
847        return Ok(());
848    }
849    let trusted_time_high_water = transaction.query_row(
850        r#"
851        SELECT trusted_time_high_water_unix_ms
852        FROM chio_channel_release_publisher_meta
853        WHERE singleton = 1
854        "#,
855        [],
856        |row| row.get::<_, i64>(0),
857    )?;
858    let detail = "deterministic transaction recovery is unavailable for a previously committed channel release dispatch";
859    for (channel_id, record_version, updated_at_unix_ms) in retained {
860        let next_version = record_version.checked_add(1).ok_or_else(|| {
861            SqliteServingOwnerError::Invalid(
862                "channel release recovery record version overflowed".to_owned(),
863            )
864        })?;
865        let changed = transaction.execute(
866            r#"
867            UPDATE chio_channel_release_publications
868            SET status = 'unknown', transaction_hash = NULL, failure_detail = ?1,
869                record_version = ?2, store_uuid = ?3, store_lease_id = ?4,
870                store_owner_epoch = ?5, updated_at_unix_ms = ?6
871            WHERE channel_id = ?7 AND status = 'dispatch_committed'
872              AND record_version = ?8
873            "#,
874            params![
875                detail,
876                next_version,
877                &serving_owner.fence.store_uuid,
878                &serving_owner.fence.lease_id,
879                i64::try_from(serving_owner.fence.owner_epoch).map_err(|_| {
880                    SqliteServingOwnerError::Invalid(
881                        "channel release recovery owner epoch exceeds SQLite range".to_owned(),
882                    )
883                })?,
884                updated_at_unix_ms.max(trusted_time_high_water),
885                &channel_id,
886                record_version,
887            ],
888        )?;
889        if changed != 1 {
890            return Err(SqliteServingOwnerError::Invalid(
891                "channel release startup quarantine lost its retained row".to_owned(),
892            ));
893        }
894        serving_owner.append_global_commit(
895            &transaction,
896            "channel_release_startup_outcome_unknown",
897            "channel_release_publication",
898            &channel_id,
899            u64::try_from(next_version).map_err(|_| {
900                SqliteServingOwnerError::Invalid(
901                    "channel release recovery record version is invalid".to_owned(),
902                )
903            })?,
904        )?;
905    }
906    transaction.commit().map_err(|error| {
907        serving_owner.outcome_unknown(format!(
908            "sqlite channel release startup quarantine outcome is unknown: {error}"
909        ))
910    })?;
911    serving_owner.sync_authority_anchor(connection)
912}
913
914#[cfg(test)]
915#[derive(Clone, Copy)]
916enum SubmissionRecord<'a> {
917    Submitted(&'a str),
918    Unknown(&'a str),
919}
920
921#[cfg(test)]
922impl<'a> SubmissionRecord<'a> {
923    const fn status(self) -> ChannelReleasePublicationStatusV1 {
924        match self {
925            Self::Submitted(_) => ChannelReleasePublicationStatusV1::Submitted,
926            Self::Unknown(_) => ChannelReleasePublicationStatusV1::Unknown,
927        }
928    }
929
930    const fn transaction_hash(self) -> Option<&'a str> {
931        match self {
932            Self::Submitted(transaction_hash) => Some(transaction_hash),
933            Self::Unknown(_) => None,
934        }
935    }
936
937    fn failure_detail(&self) -> Option<&'a str> {
938        match *self {
939            Self::Submitted(_) => None,
940            Self::Unknown(detail) => Some(detail),
941        }
942    }
943}
944
945#[cfg(test)]
946#[path = "channel_release_publisher_store_tests.rs"]
947mod tests;