Skip to main content

chio_store_sqlite/frost_store/
mod.rs

1use std::sync::{Arc, Mutex, MutexGuard};
2
3use chio_core::StoreMutationFence;
4use chio_federation_authority::{FrostAuthenticatedDkgPackage, FrostCeremonySecret};
5use rusqlite::{Connection, Transaction, TransactionBehavior};
6use serde::{Deserialize, Serialize};
7
8use crate::admission_operation_store::verify_active_owner;
9use crate::encrypted_blob::TenantKey;
10use crate::serving_owner::SqliteServingOwner;
11
12mod ceremony;
13mod commit;
14mod coordinator;
15mod rotation;
16mod rotation_validation;
17mod schema;
18mod signer;
19
20use schema::FROST_STORE_SCHEMA;
21
22const FROST_STORE_SCHEMA_KEY: &str = "frost";
23pub(crate) const FROST_STORE_SUPPORTED_SCHEMA_VERSION: i32 = 0;
24const FROST_STORE_SCHEMA_ANCHORS: &[&str] = &[
25    "frost_ceremonies",
26    "chio_serving_owner",
27    "capability_grant_budgets",
28];
29
30#[derive(Debug, thiserror::Error)]
31pub enum FrostStoreError {
32    #[error("sqlite FROST store is fenced")]
33    Fenced,
34    #[error("sqlite FROST store conflict: {0}")]
35    Conflict(&'static str),
36    #[error("sqlite FROST store state is invalid: {0}")]
37    InvalidState(String),
38    #[error("sqlite FROST custody failed: {0}")]
39    Custody(&'static str),
40    #[error("sqlite FROST store unavailable: {0}")]
41    Unavailable(String),
42    #[error(transparent)]
43    Ceremony(#[from] chio_federation_authority::FrostCeremonyError),
44}
45
46pub struct FrostCustodyKey {
47    generation: String,
48    key: TenantKey,
49}
50
51impl std::fmt::Debug for FrostCustodyKey {
52    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        formatter
54            .debug_struct("FrostCustodyKey")
55            .field("generation", &self.generation)
56            .field("key", &"<redacted>")
57            .finish()
58    }
59}
60
61impl FrostCustodyKey {
62    pub fn new(
63        generation: impl Into<String>,
64        key_bytes: [u8; 32],
65    ) -> Result<Self, FrostStoreError> {
66        let generation = generation.into();
67        if generation.is_empty()
68            || generation.len() > 128
69            || generation.trim() != generation
70            || !generation.bytes().all(|byte| byte.is_ascii_graphic())
71        {
72            return Err(FrostStoreError::Custody(
73                "generation must be unpadded printable ASCII",
74            ));
75        }
76        Ok(Self {
77            generation,
78            key: TenantKey::from_bytes(key_bytes),
79        })
80    }
81
82    #[must_use]
83    pub fn generation(&self) -> &str {
84        &self.generation
85    }
86
87    pub(super) fn key(&self) -> &TenantKey {
88        &self.key
89    }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case")]
94pub enum FrostCeremonyState {
95    Round1Ready,
96    Round2Ready,
97    Completed,
98}
99
100impl FrostCeremonyState {
101    pub(super) const fn as_str(self) -> &'static str {
102        match self {
103            Self::Round1Ready => "round1_ready",
104            Self::Round2Ready => "round2_ready",
105            Self::Completed => "completed",
106        }
107    }
108
109    pub(super) fn parse(value: &str) -> Result<Self, FrostStoreError> {
110        match value {
111            "round1_ready" => Ok(Self::Round1Ready),
112            "round2_ready" => Ok(Self::Round2Ready),
113            "completed" => Ok(Self::Completed),
114            _ => Err(FrostStoreError::InvalidState(
115                "unknown ceremony state".to_string(),
116            )),
117        }
118    }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct FrostCeremonyRecord {
123    pub ceremony_id: String,
124    pub state: FrostCeremonyState,
125    pub state_version: u64,
126    pub participant_set_digest: String,
127    pub scope_id: String,
128    pub key_epoch: u64,
129    pub local_participant_id: String,
130    pub input_transcript_digest: Option<String>,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct FrostCeremonyRound1Record {
135    pub ceremony_id: String,
136    pub state: FrostCeremonyState,
137    pub state_version: u64,
138    pub package: FrostAuthenticatedDkgPackage,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct FrostCeremonyRound2Record {
143    pub ceremony_id: String,
144    pub state: FrostCeremonyState,
145    pub state_version: u64,
146    pub packages: Vec<FrostAuthenticatedDkgPackage>,
147    pub round1_transcript_digest: String,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct StoredFrostCeremonyCompletion {
152    pub ceremony_id: String,
153    pub state: FrostCeremonyState,
154    pub state_version: u64,
155    pub public_key_package: Vec<u8>,
156    pub group_public_key: String,
157    pub verification_shares: std::collections::BTreeMap<String, String>,
158    pub transcript_digest: String,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case")]
163pub enum FrostRotationState {
164    Staged,
165    AnchorAdvanced,
166    Active,
167    Discarded,
168}
169
170impl FrostRotationState {
171    pub(super) const fn as_str(self) -> &'static str {
172        match self {
173            Self::Staged => "staged",
174            Self::AnchorAdvanced => "anchor_advanced",
175            Self::Active => "active",
176            Self::Discarded => "discarded",
177        }
178    }
179
180    pub(super) fn parse(value: &str) -> Result<Self, FrostStoreError> {
181        match value {
182            "staged" => Ok(Self::Staged),
183            "anchor_advanced" => Ok(Self::AnchorAdvanced),
184            "active" => Ok(Self::Active),
185            "discarded" => Ok(Self::Discarded),
186            _ => Err(FrostStoreError::InvalidState(
187                "unknown FROST rotation state".to_string(),
188            )),
189        }
190    }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct FrostActiveRosterRecord {
195    pub scope_id: String,
196    pub key_epoch: u64,
197    pub roster_digest: String,
198    pub checkpoint_sequence: u64,
199    pub checkpoint_digest: String,
200    pub activation_fence: u64,
201    pub clock_high_water: u64,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct FrostRotationRecord {
206    pub rotation_id: String,
207    pub scope_id: String,
208    pub state: FrostRotationState,
209    pub state_version: u64,
210    pub predecessor_checkpoint_digest: String,
211    pub target_roster_digest: String,
212    pub target_key_epoch: u64,
213    pub anchored_checkpoint_digest: Option<String>,
214}
215
216#[derive(Debug, Clone)]
217pub struct StagedFrostRotation {
218    rotation_id: String,
219    advance: chio_federation::frost::VerifiedFrostEpochAdvance,
220}
221
222pub struct FrostSignerSessionRequest<'a> {
223    pub body: &'a chio_federation::frost::FrostAuthorizationBodyV1,
224    pub active_roster: &'a chio_federation::frost::VerifiedActiveFrostRoster,
225    pub epoch_anchor: &'a dyn chio_federation::frost::FrostEpochAnchor,
226    pub slot_anchor: &'a dyn chio_federation::frost::FrostAuthorizationSlotAnchorWriter,
227    pub artifact_trust: &'a chio_federation::frost::FrostArtifactTrustStore,
228    pub ceremony_id: &'a str,
229    pub participant_id: &'a str,
230    pub coordinator_id: &'a str,
231}
232
233pub struct FrostCoordinatorSessionRequest<'a> {
234    pub body: &'a chio_federation::frost::FrostAuthorizationBodyV1,
235    pub active_roster: &'a chio_federation::frost::VerifiedActiveFrostRoster,
236    pub epoch_anchor: &'a dyn chio_federation::frost::FrostEpochAnchor,
237    pub slot_anchor: &'a dyn chio_federation::frost::FrostAuthorizationSlotAnchorWriter,
238    pub artifact_trust: &'a chio_federation::frost::FrostArtifactTrustStore,
239    pub coordinator_id: &'a str,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243#[serde(rename_all = "camelCase", deny_unknown_fields)]
244pub struct FrostCoordinatorLease {
245    pub session_id: String,
246    pub authorization_slot_id: String,
247    pub coordinator_id: String,
248    pub worker_id: String,
249    pub lease_id: String,
250    pub owner_epoch: u64,
251    pub expires_at_unix_ms: u64,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase", deny_unknown_fields)]
256pub struct FrostCoordinatorCommitment {
257    pub participant_id: String,
258    pub signer_identifier: Vec<u8>,
259    pub commitment_bytes: Vec<u8>,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase", deny_unknown_fields)]
264pub struct FrostCoordinatorShare {
265    pub participant_id: String,
266    pub share_bytes: Vec<u8>,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270#[serde(rename_all = "camelCase", deny_unknown_fields)]
271pub struct FrostCoordinatorSigningPackage {
272    pub session_id: String,
273    pub participant_ids: Vec<String>,
274    pub signing_package_bytes: Vec<u8>,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase", deny_unknown_fields)]
279pub struct FrostCoordinatorSessionRecord {
280    pub session_id: String,
281    pub authorization_slot_id: String,
282    pub state: FrostCoordinatorSessionState,
283    pub row_version: u64,
284    pub commitment_count: usize,
285    pub share_count: usize,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289#[serde(rename_all = "camelCase", deny_unknown_fields)]
290pub struct FrostCoordinatorCancellation {
291    pub session: FrostCoordinatorSessionRecord,
292    pub participant_ids: Vec<String>,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
296#[serde(rename_all = "snake_case")]
297pub enum FrostCoordinatorSessionState {
298    CollectingCommitments,
299    PackageReady,
300    AuthorizationReady,
301    Completed,
302    Burned,
303}
304
305impl FrostCoordinatorSessionState {
306    #[must_use]
307    pub const fn as_str(self) -> &'static str {
308        match self {
309            Self::CollectingCommitments => "collecting_commitments",
310            Self::PackageReady => "package_ready",
311            Self::AuthorizationReady => "authorization_ready",
312            Self::Completed => "completed",
313            Self::Burned => "burned",
314        }
315    }
316
317    pub(super) fn parse(value: &str) -> Result<Self, FrostStoreError> {
318        match value {
319            "collecting_commitments" => Ok(Self::CollectingCommitments),
320            "package_ready" => Ok(Self::PackageReady),
321            "authorization_ready" => Ok(Self::AuthorizationReady),
322            "completed" => Ok(Self::Completed),
323            "burned" => Ok(Self::Burned),
324            _ => Err(FrostStoreError::InvalidState(
325                "unknown FROST coordinator state".to_string(),
326            )),
327        }
328    }
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct FrostSignerSessionRecord {
333    pub session_id: String,
334    pub participant_id: String,
335    pub authorization_slot_id: String,
336    pub state: FrostSignerSessionState,
337    pub state_version: u64,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq)]
341pub struct FrostSignerCommitment {
342    pub session_id: String,
343    pub participant_id: String,
344    pub signer_identifier: Vec<u8>,
345    pub commitment_bytes: Vec<u8>,
346}
347
348#[derive(Debug, Clone, PartialEq, Eq)]
349pub struct FrostSignerShare {
350    pub session_id: String,
351    pub participant_id: String,
352    pub share_bytes: Vec<u8>,
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
356#[serde(rename_all = "snake_case")]
357pub enum FrostSignerSessionState {
358    Prepared,
359    CommitmentPublished,
360    ShareReady,
361    Completed,
362    Burned,
363}
364
365impl FrostSignerSessionState {
366    #[must_use]
367    pub const fn as_str(self) -> &'static str {
368        match self {
369            Self::Prepared => "prepared",
370            Self::CommitmentPublished => "commitment_published",
371            Self::ShareReady => "share_ready",
372            Self::Completed => "completed",
373            Self::Burned => "burned",
374        }
375    }
376
377    pub(super) fn parse(value: &str) -> Result<Self, FrostStoreError> {
378        match value {
379            "prepared" => Ok(Self::Prepared),
380            "commitment_published" => Ok(Self::CommitmentPublished),
381            "share_ready" => Ok(Self::ShareReady),
382            "completed" => Ok(Self::Completed),
383            "burned" => Ok(Self::Burned),
384            _ => Err(FrostStoreError::InvalidState(
385                "unknown FROST signer state".to_string(),
386            )),
387        }
388    }
389}
390
391impl StagedFrostRotation {
392    #[must_use]
393    pub fn rotation_id(&self) -> &str {
394        &self.rotation_id
395    }
396
397    #[must_use]
398    pub fn advance(&self) -> &chio_federation::frost::VerifiedFrostEpochAdvance {
399        &self.advance
400    }
401}
402
403#[derive(Clone)]
404pub struct SqliteFrostStore {
405    connection: Arc<Mutex<Connection>>,
406    serving_owner: Arc<SqliteServingOwner>,
407}
408
409impl SqliteFrostStore {
410    pub(crate) fn open_alongside(
411        connection: Arc<Mutex<Connection>>,
412        serving_owner: Arc<SqliteServingOwner>,
413    ) -> Self {
414        Self {
415            connection,
416            serving_owner,
417        }
418    }
419
420    fn connection(&self) -> Result<MutexGuard<'_, Connection>, FrostStoreError> {
421        self.connection.lock().map_err(|_| {
422            FrostStoreError::Unavailable("sqlite FROST store lock is poisoned".to_string())
423        })
424    }
425
426    fn begin_read<'a>(
427        &self,
428        connection: &'a mut Connection,
429        fence: Option<&StoreMutationFence>,
430    ) -> Result<Transaction<'a>, FrostStoreError> {
431        let transaction = connection
432            .transaction_with_behavior(TransactionBehavior::Deferred)
433            .map_err(sqlite_error)?;
434        verify_active_owner(&transaction, &self.serving_owner, fence).map_err(owner_error)?;
435        self.serving_owner
436            .verify_authority_anchor(&transaction)
437            .map_err(|error| FrostStoreError::Unavailable(error.to_string()))?;
438        Ok(transaction)
439    }
440
441    fn begin_write<'a>(
442        &self,
443        connection: &'a mut Connection,
444        fence: &StoreMutationFence,
445    ) -> Result<Transaction<'a>, FrostStoreError> {
446        let transaction = connection
447            .transaction_with_behavior(TransactionBehavior::Immediate)
448            .map_err(sqlite_error)?;
449        verify_active_owner(&transaction, &self.serving_owner, Some(fence)).map_err(owner_error)?;
450        self.serving_owner
451            .verify_authority_anchor(&transaction)
452            .map_err(|error| FrostStoreError::Unavailable(error.to_string()))?;
453        Ok(transaction)
454    }
455
456    fn commit_write(&self, transaction: Transaction<'_>) -> Result<(), FrostStoreError> {
457        transaction.commit().map_err(|error| {
458            FrostStoreError::Unavailable(
459                self.serving_owner
460                    .outcome_unknown(format!("sqlite FROST commit outcome is unknown: {error}"))
461                    .to_string(),
462            )
463        })
464    }
465
466    fn sync_after_write(&self, connection: &Connection) -> Result<(), FrostStoreError> {
467        self.serving_owner
468            .sync_authority_anchor(connection)
469            .map_err(|error| FrostStoreError::Unavailable(error.to_string()))
470    }
471}
472
473pub(crate) fn initialize_frost_schema(connection: &mut Connection) -> Result<(), FrostStoreError> {
474    crate::check_schema_version(
475        connection,
476        FROST_STORE_SCHEMA_KEY,
477        FROST_STORE_SUPPORTED_SCHEMA_VERSION,
478        FROST_STORE_SCHEMA_ANCHORS,
479    )
480    .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
481    let transaction = connection
482        .transaction_with_behavior(TransactionBehavior::Immediate)
483        .map_err(sqlite_error)?;
484    transaction
485        .execute_batch(FROST_STORE_SCHEMA)
486        .map_err(sqlite_error)?;
487    crate::stamp_schema_version(
488        &transaction,
489        FROST_STORE_SCHEMA_KEY,
490        FROST_STORE_SUPPORTED_SCHEMA_VERSION,
491    )
492    .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
493    verify_frost_store_invariants(&transaction)?;
494    transaction.commit().map_err(sqlite_error)
495}
496
497pub(crate) fn verify_frost_store_invariants(
498    connection: &Connection,
499) -> Result<(), FrostStoreError> {
500    let expected = Connection::open_in_memory().map_err(sqlite_error)?;
501    expected
502        .execute_batch(FROST_STORE_SCHEMA)
503        .map_err(sqlite_error)?;
504    if frost_schema_catalog(connection)? != frost_schema_catalog(&expected)? {
505        return Err(FrostStoreError::InvalidState(
506            "FROST store schema differs from the canonical definition".to_string(),
507        ));
508    }
509    ceremony::verify_ceremony_invariants(connection)?;
510    rotation::verify_rotation_invariants(connection)?;
511    signer::verify_signer_invariants(connection)?;
512    coordinator::verify_coordinator_invariants(connection)
513}
514
515type FrostSchemaCatalogEntry = (String, String, String, Option<String>);
516
517fn frost_schema_catalog(
518    connection: &Connection,
519) -> Result<Vec<FrostSchemaCatalogEntry>, FrostStoreError> {
520    let mut statement = connection
521        .prepare(
522            r#"
523            SELECT type, name, tbl_name, sql FROM sqlite_schema
524            WHERE name GLOB 'frost_*' OR tbl_name GLOB 'frost_*'
525            ORDER BY type, name, tbl_name
526            "#,
527        )
528        .map_err(sqlite_error)?;
529    let catalog = statement
530        .query_map([], |row| {
531            Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
532        })
533        .map_err(sqlite_error)?
534        .collect::<Result<Vec<_>, _>>()
535        .map_err(sqlite_error)?;
536    Ok(catalog)
537}
538
539fn sqlite_error(error: rusqlite::Error) -> FrostStoreError {
540    FrostStoreError::Unavailable(error.to_string())
541}
542
543fn owner_error(
544    error: chio_kernel::admission_operation::AdmissionOperationStoreError,
545) -> FrostStoreError {
546    if matches!(
547        error,
548        chio_kernel::admission_operation::AdmissionOperationStoreError::Fenced
549    ) {
550        FrostStoreError::Fenced
551    } else {
552        FrostStoreError::Unavailable(error.to_string())
553    }
554}
555
556#[derive(Serialize, Deserialize)]
557#[serde(tag = "state", content = "output", rename_all = "snake_case")]
558enum StoredCeremonyOutput {
559    Round1(Box<FrostAuthenticatedDkgPackage>),
560    Round2(Vec<FrostAuthenticatedDkgPackage>),
561}
562
563pub(super) fn secret_kind_name(secret: &FrostCeremonySecret) -> &'static str {
564    match secret.kind() {
565        chio_federation_authority::FrostCeremonySecretKind::Round1 => "round1",
566        chio_federation_authority::FrostCeremonySecretKind::Round2 => "round2",
567        chio_federation_authority::FrostCeremonySecretKind::KeyPackage => "key_package",
568    }
569}