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;