Skip to main content

chio_store_sqlite/frost_store/
ceremony.rs

1use std::collections::BTreeMap;
2
3use chio_core::canonical::canonical_json_bytes;
4use chio_core::{sha256_hex, StoreMutationFence};
5use chio_federation_authority::{
6    advance_frost_ceremony, begin_frost_ceremony, complete_frost_ceremony,
7    verify_frost_ceremony_round1_transcript, verify_frost_ceremony_transcript,
8    FrostAuthenticatedDkgPackage, FrostCeremonyConfig, FrostCeremonySecret,
9    FrostCeremonySecretKind,
10};
11use rand_core::{CryptoRng, RngCore};
12use rusqlite::{params, Connection, OptionalExtension, Row, Transaction};
13use serde::Serialize;
14use zeroize::Zeroizing;
15
16use super::commit::{
17    append_projection_commit, prefixed_digest, verify_projection_commit_chains, ProjectionMutation,
18};
19use super::{
20    secret_kind_name, FrostCeremonyRecord, FrostCeremonyRound1Record, FrostCeremonyRound2Record,
21    FrostCeremonyState, FrostCustodyKey, FrostStoreError, SqliteFrostStore, StoredCeremonyOutput,
22    StoredFrostCeremonyCompletion,
23};
24use crate::encrypted_blob::{decrypt_blob_with_aad, try_encrypt_blob_with_aad, EncryptedBlob};
25
26const CUSTODY_AAD_FORMAT: &str = "chio.frost.ceremony-custody-aad.v1";
27const RECORD_DIGEST_PREFIX: &[u8] = b"chio.frost.ceremony-record.digest.v1\0";
28
29#[derive(Debug)]
30struct StoredCeremonyRow {
31    ceremony_id: String,
32    config_json: Vec<u8>,
33    config_digest: String,
34    participant_set_digest: String,
35    scope_id: String,
36    key_epoch: u64,
37    local_participant_id: String,
38    state: FrostCeremonyState,
39    state_version: u64,
40    custody_generation: String,
41    secret_kind: String,
42    secret: EncryptedBlob,
43    output: Option<EncryptedBlob>,
44    input_transcript_digest: Option<String>,
45    public_key_package: Option<Vec<u8>>,
46    group_public_key: Option<String>,
47    verification_shares_json: Option<Vec<u8>>,
48    source_fence: StoreMutationFence,
49    updated_at_unix_ms: u64,
50    completed_at_unix_ms: Option<u64>,
51    record_digest: String,
52}
53
54struct CeremonyWrite<'a> {
55    ceremony_id: &'a str,
56    config_json: &'a [u8],
57    config_digest: &'a str,
58    participant_set_digest: &'a str,
59    scope_id: &'a str,
60    key_epoch: u64,
61    local_participant_id: &'a str,
62    state: FrostCeremonyState,
63    state_version: u64,
64    custody_generation: &'a str,
65    secret_kind: &'a str,
66    secret: &'a EncryptedBlob,
67    output: Option<&'a EncryptedBlob>,
68    input_transcript_digest: Option<&'a str>,
69    public_key_package: Option<&'a [u8]>,
70    group_public_key: Option<&'a str>,
71    verification_shares_json: Option<&'a [u8]>,
72    source_fence: &'a StoreMutationFence,
73    updated_at_unix_ms: u64,
74    completed_at_unix_ms: Option<u64>,
75}
76
77struct CustodyBinding<'a> {
78    ceremony_id: &'a str,
79    config_digest: &'a str,
80    state: FrostCeremonyState,
81    state_version: u64,
82    custody_generation: &'a str,
83    fence: &'a StoreMutationFence,
84    updated_at_unix_ms: u64,
85}
86
87impl StoredCeremonyRow {
88    fn custody_binding(&self) -> CustodyBinding<'_> {
89        CustodyBinding {
90            ceremony_id: &self.ceremony_id,
91            config_digest: &self.config_digest,
92            state: self.state,
93            state_version: self.state_version,
94            custody_generation: &self.custody_generation,
95            fence: &self.source_fence,
96            updated_at_unix_ms: self.updated_at_unix_ms,
97        }
98    }
99}
100
101#[derive(Serialize)]
102#[serde(rename_all = "camelCase")]
103struct CustodyAad<'a> {
104    format: &'static str,
105    purpose: &'a str,
106    ceremony_id: &'a str,
107    config_digest: &'a str,
108    state: FrostCeremonyState,
109    state_version: u64,
110    custody_generation: &'a str,
111    store_uuid: &'a str,
112    store_lease_id: &'a str,
113    store_owner_epoch: u64,
114    updated_at_unix_ms: u64,
115}
116
117#[derive(Serialize)]
118#[serde(rename_all = "camelCase")]
119struct RecordDigestPreimage<'a> {
120    format: &'static str,
121    ceremony_id: &'a str,
122    config_digest: &'a str,
123    participant_set_digest: &'a str,
124    scope_id: &'a str,
125    key_epoch: u64,
126    local_participant_id: &'a str,
127    state: FrostCeremonyState,
128    state_version: u64,
129    custody_generation: &'a str,
130    secret_kind: &'a str,
131    secret_nonce: String,
132    secret_ciphertext_digest: String,
133    output_nonce: Option<String>,
134    output_ciphertext_digest: Option<String>,
135    input_transcript_digest: Option<&'a str>,
136    public_key_package_digest: Option<String>,
137    group_public_key: Option<&'a str>,
138    verification_shares_digest: Option<String>,
139    store_uuid: &'a str,
140    store_lease_id: &'a str,
141    store_owner_epoch: u64,
142    updated_at_unix_ms: u64,
143    completed_at_unix_ms: Option<u64>,
144}
145
146impl SqliteFrostStore {
147    #[allow(clippy::too_many_arguments)]
148    pub fn begin_ceremony<R: CryptoRng + RngCore>(
149        &self,
150        config: &FrostCeremonyConfig,
151        transport_key: &chio_core::Keypair,
152        custody: &FrostCustodyKey,
153        rng: &mut R,
154        fence: &StoreMutationFence,
155        trusted_now_unix_ms: u64,
156    ) -> Result<FrostCeremonyRound1Record, FrostStoreError> {
157        validate_trusted_time(trusted_now_unix_ms)?;
158        let ceremony_id = config.ceremony_id()?;
159        let config_json = canonical_json_bytes(config)
160            .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
161        let config_digest = sha256_hex(&config_json);
162        let participant_set_digest = config.participant_set_digest()?;
163
164        let mut connection = self.connection()?;
165        let transaction = self.begin_write(&mut connection, fence)?;
166        if let Some(stored) = load_ceremony_tx(&transaction, &ceremony_id)? {
167            verify_exact_config(&stored, &config_digest, custody, trusted_now_unix_ms)?;
168            if stored.state != FrostCeremonyState::Round1Ready {
169                return Err(FrostStoreError::Conflict(
170                    "ceremony already advanced beyond round one",
171                ));
172            }
173            let output = decrypt_output(&stored, custody)?;
174            let StoredCeremonyOutput::Round1(package) = output else {
175                return Err(invalid("round-one state retained another output kind"));
176            };
177            transaction.commit().map_err(super::sqlite_error)?;
178            return Ok(FrostCeremonyRound1Record {
179                ceremony_id,
180                state: stored.state,
181                state_version: stored.state_version,
182                package: *package,
183            });
184        }
185        let conflicting = transaction
186            .query_row(
187                r#"
188                SELECT ceremony_id FROM frost_ceremonies
189                WHERE scope_id = ?1 AND key_epoch = ?2 AND local_participant_id = ?3
190                "#,
191                params![
192                    &config.scope_id,
193                    sqlite_u64(config.key_epoch, "ceremony key epoch")?,
194                    &config.local_participant_id,
195                ],
196                |row| row.get::<_, String>(0),
197            )
198            .optional()
199            .map_err(super::sqlite_error)?;
200        if conflicting.is_some() {
201            return Err(FrostStoreError::Conflict(
202                "another participant set already owns this scope and epoch",
203            ));
204        }
205
206        let transition = begin_frost_ceremony(config, transport_key, rng)?;
207        let state = FrostCeremonyState::Round1Ready;
208        let state_version = 1;
209        let custody_binding = CustodyBinding {
210            ceremony_id: &ceremony_id,
211            config_digest: &config_digest,
212            state,
213            state_version,
214            custody_generation: custody.generation(),
215            fence,
216            updated_at_unix_ms: trusted_now_unix_ms,
217        };
218        let secret = encrypt_material(
219            transition.secret.custody_bytes(),
220            custody,
221            &custody_aad("secret", &custody_binding)?,
222        )?;
223        let output_bytes = Zeroizing::new(
224            canonical_json_bytes(&StoredCeremonyOutput::Round1(Box::new(
225                transition.package.clone(),
226            )))
227            .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?,
228        );
229        let output = encrypt_material(
230            &output_bytes,
231            custody,
232            &custody_aad("output", &custody_binding)?,
233        )?;
234        let write = CeremonyWrite {
235            ceremony_id: &ceremony_id,
236            config_json: &config_json,
237            config_digest: &config_digest,
238            participant_set_digest: &participant_set_digest,
239            scope_id: &config.scope_id,
240            key_epoch: config.key_epoch,
241            local_participant_id: &config.local_participant_id,
242            state,
243            state_version,
244            custody_generation: custody.generation(),
245            secret_kind: secret_kind_name(&transition.secret),
246            secret: &secret,
247            output: Some(&output),
248            input_transcript_digest: None,
249            public_key_package: None,
250            group_public_key: None,
251            verification_shares_json: None,
252            source_fence: fence,
253            updated_at_unix_ms: trusted_now_unix_ms,
254            completed_at_unix_ms: None,
255        };
256        let record_digest = record_digest(&write)?;
257        insert_ceremony(&transaction, &write, &record_digest)?;
258        append_projection_commit(
259            &transaction,
260            self,
261            &projection_key(&ceremony_id),
262            ProjectionMutation {
263                sequence: state_version,
264                projection_type: "ceremony",
265                mutation_kind: "frost.ceremony.begin",
266                record_digest: &record_digest,
267            },
268            fence,
269        )?;
270        self.commit_write(transaction)?;
271        self.sync_after_write(&connection)?;
272        Ok(FrostCeremonyRound1Record {
273            ceremony_id,
274            state,
275            state_version,
276            package: transition.package,
277        })
278    }
279
280    #[allow(clippy::too_many_arguments)]
281    pub fn advance_ceremony(
282        &self,
283        config: &FrostCeremonyConfig,
284        transport_key: &chio_core::Keypair,
285        custody: &FrostCustodyKey,
286        round1_packages: &[FrostAuthenticatedDkgPackage],
287        fence: &StoreMutationFence,
288        trusted_now_unix_ms: u64,
289    ) -> Result<FrostCeremonyRound2Record, FrostStoreError> {
290        validate_trusted_time(trusted_now_unix_ms)?;
291        let ceremony_id = config.ceremony_id()?;
292        let config_json = canonical_json_bytes(config)
293            .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
294        let config_digest = sha256_hex(&config_json);
295        let round1_digest = verify_frost_ceremony_round1_transcript(config, round1_packages)?;
296
297        let mut connection = self.connection()?;
298        let transaction = self.begin_write(&mut connection, fence)?;
299        let stored = load_ceremony_tx(&transaction, &ceremony_id)?
300            .ok_or(FrostStoreError::Conflict("ceremony has not started"))?;
301        verify_exact_config(&stored, &config_digest, custody, trusted_now_unix_ms)?;
302        if stored.state == FrostCeremonyState::Round2Ready {
303            if stored.input_transcript_digest.as_deref() != Some(&round1_digest) {
304                return Err(FrostStoreError::Conflict(
305                    "round-one transcript changed after advancement",
306                ));
307            }
308            let output = decrypt_output(&stored, custody)?;
309            let StoredCeremonyOutput::Round2(packages) = output else {
310                return Err(invalid("round-two state retained another output kind"));
311            };
312            transaction.commit().map_err(super::sqlite_error)?;
313            return Ok(FrostCeremonyRound2Record {
314                ceremony_id,
315                state: stored.state,
316                state_version: stored.state_version,
317                packages,
318                round1_transcript_digest: round1_digest,
319            });
320        }
321        if stored.state != FrostCeremonyState::Round1Ready || stored.state_version != 1 {
322            return Err(FrostStoreError::Conflict(
323                "ceremony cannot advance from its current state",
324            ));
325        }
326        let round1_secret = decrypt_secret(&stored, custody)?;
327        let transition =
328            advance_frost_ceremony(config, transport_key, round1_secret, round1_packages)?;
329        if transition.round1_transcript_digest != round1_digest {
330            return Err(invalid(
331                "authority returned a different round-one transcript",
332            ));
333        }
334        let state = FrostCeremonyState::Round2Ready;
335        let state_version = 2;
336        let custody_binding = CustodyBinding {
337            ceremony_id: &ceremony_id,
338            config_digest: &config_digest,
339            state,
340            state_version,
341            custody_generation: custody.generation(),
342            fence,
343            updated_at_unix_ms: trusted_now_unix_ms,
344        };
345        let secret = encrypt_material(
346            transition.secret.custody_bytes(),
347            custody,
348            &custody_aad("secret", &custody_binding)?,
349        )?;
350        let output_bytes = Zeroizing::new(
351            canonical_json_bytes(&StoredCeremonyOutput::Round2(transition.packages.clone()))
352                .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?,
353        );
354        let output = encrypt_material(
355            &output_bytes,
356            custody,
357            &custody_aad("output", &custody_binding)?,
358        )?;
359        let write = CeremonyWrite {
360            ceremony_id: &ceremony_id,
361            config_json: &config_json,
362            config_digest: &config_digest,
363            participant_set_digest: &stored.participant_set_digest,
364            scope_id: &stored.scope_id,
365            key_epoch: stored.key_epoch,
366            local_participant_id: &stored.local_participant_id,
367            state,
368            state_version,
369            custody_generation: custody.generation(),
370            secret_kind: secret_kind_name(&transition.secret),
371            secret: &secret,
372            output: Some(&output),
373            input_transcript_digest: Some(&round1_digest),
374            public_key_package: None,
375            group_public_key: None,
376            verification_shares_json: None,
377            source_fence: fence,
378            updated_at_unix_ms: trusted_now_unix_ms,
379            completed_at_unix_ms: None,
380        };
381        let record_digest = record_digest(&write)?;
382        update_ceremony(&transaction, &write, &record_digest, 1)?;
383        append_projection_commit(
384            &transaction,
385            self,
386            &projection_key(&ceremony_id),
387            ProjectionMutation {
388                sequence: state_version,
389                projection_type: "ceremony",
390                mutation_kind: "frost.ceremony.advance",
391                record_digest: &record_digest,
392            },
393            fence,
394        )?;
395        self.commit_write(transaction)?;
396        self.sync_after_write(&connection)?;
397        Ok(FrostCeremonyRound2Record {
398            ceremony_id,
399            state,
400            state_version,
401            packages: transition.packages,
402            round1_transcript_digest: round1_digest,
403        })
404    }
405
406    pub fn complete_ceremony(
407        &self,
408        config: &FrostCeremonyConfig,
409        custody: &FrostCustodyKey,
410        round1_packages: &[FrostAuthenticatedDkgPackage],
411        round2_packages: &[FrostAuthenticatedDkgPackage],
412        fence: &StoreMutationFence,
413        trusted_now_unix_ms: u64,
414    ) -> Result<StoredFrostCeremonyCompletion, FrostStoreError> {
415        validate_trusted_time(trusted_now_unix_ms)?;
416        let ceremony_id = config.ceremony_id()?;
417        let config_json = canonical_json_bytes(config)
418            .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
419        let config_digest = sha256_hex(&config_json);
420        let transcript_digest =
421            verify_frost_ceremony_transcript(config, round1_packages, round2_packages)?;
422
423        let mut connection = self.connection()?;
424        let transaction = self.begin_write(&mut connection, fence)?;
425        let stored = load_ceremony_tx(&transaction, &ceremony_id)?
426            .ok_or(FrostStoreError::Conflict("ceremony has not started"))?;
427        verify_exact_config(&stored, &config_digest, custody, trusted_now_unix_ms)?;
428        if stored.state == FrostCeremonyState::Completed {
429            if stored.input_transcript_digest.as_deref() != Some(&transcript_digest) {
430                return Err(FrostStoreError::Conflict(
431                    "ceremony transcript changed after completion",
432                ));
433            }
434            let completion = completion_from_row(&stored)?;
435            transaction.commit().map_err(super::sqlite_error)?;
436            return Ok(completion);
437        }
438        if stored.state != FrostCeremonyState::Round2Ready || stored.state_version != 2 {
439            return Err(FrostStoreError::Conflict(
440                "ceremony cannot complete from its current state",
441            ));
442        }
443        let round2_secret = decrypt_secret(&stored, custody)?;
444        let completion =
445            complete_frost_ceremony(config, round2_secret, round1_packages, round2_packages)?;
446        if completion.transcript_digest != transcript_digest {
447            return Err(invalid(
448                "authority returned a different ceremony transcript",
449            ));
450        }
451        let state = FrostCeremonyState::Completed;
452        let state_version = 3;
453        let custody_binding = CustodyBinding {
454            ceremony_id: &ceremony_id,
455            config_digest: &config_digest,
456            state,
457            state_version,
458            custody_generation: custody.generation(),
459            fence,
460            updated_at_unix_ms: trusted_now_unix_ms,
461        };
462        let secret = encrypt_material(
463            completion.key_package.custody_bytes(),
464            custody,
465            &custody_aad("secret", &custody_binding)?,
466        )?;
467        let verification_shares_json = canonical_json_bytes(&completion.verification_shares)
468            .map_err(|error| FrostStoreError::InvalidState(error.to_string()))?;
469        let write = CeremonyWrite {
470            ceremony_id: &ceremony_id,
471            config_json: &config_json,
472            config_digest: &config_digest,
473            participant_set_digest: &stored.participant_set_digest,
474            scope_id: &stored.scope_id,
475            key_epoch: stored.key_epoch,
476            local_participant_id: &stored.local_participant_id,
477            state,
478            state_version,
479            custody_generation: custody.generation(),
480            secret_kind: secret_kind_name(&completion.key_package),
481            secret: &secret,
482            output: None,
483            input_transcript_digest: Some(&transcript_digest),
484            public_key_package: Some(&completion.public_key_package),
485            group_public_key: Some(&completion.group_public_key),
486            verification_shares_json: Some(&verification_shares_json),
487            source_fence: fence,
488            updated_at_unix_ms: trusted_now_unix_ms,
489            completed_at_unix_ms: Some(trusted_now_unix_ms),
490        };
491        let record_digest = record_digest(&write)?;
492        update_ceremony(&transaction, &write, &record_digest, 2)?;
493        append_projection_commit(
494            &transaction,
495            self,
496            &projection_key(&ceremony_id),
497            ProjectionMutation {
498                sequence: state_version,
499                projection_type: "ceremony",
500                mutation_kind: "frost.ceremony.complete",
501                record_digest: &record_digest,
502            },
503            fence,
504        )?;
505        self.commit_write(transaction)?;
506        self.sync_after_write(&connection)?;
507        Ok(StoredFrostCeremonyCompletion {
508            ceremony_id,
509            state,
510            state_version,
511            public_key_package: completion.public_key_package,
512            group_public_key: completion.group_public_key,
513            verification_shares: completion.verification_shares,
514            transcript_digest,
515        })
516    }
517
518    pub fn load_ceremony(
519        &self,
520        ceremony_id: &str,
521        custody: &FrostCustodyKey,
522    ) -> Result<Option<FrostCeremonyRecord>, FrostStoreError> {
523        let mut connection = self.connection()?;
524        let transaction = self.begin_read(&mut connection, None)?;
525        let Some(stored) = load_ceremony_tx(&transaction, ceremony_id)? else {
526            transaction.commit().map_err(super::sqlite_error)?;
527            return Ok(None);
528        };
529        drop(decrypt_secret(&stored, custody)?);
530        let record = public_record(&stored);
531        transaction.commit().map_err(super::sqlite_error)?;
532        Ok(Some(record))
533    }
534
535    pub fn load_completed_key_package(
536        &self,
537        ceremony_id: &str,
538        custody: &FrostCustodyKey,
539        fence: &StoreMutationFence,
540    ) -> Result<FrostCeremonySecret, FrostStoreError> {
541        let mut connection = self.connection()?;
542        let transaction = self.begin_read(&mut connection, Some(fence))?;
543        let stored = load_ceremony_tx(&transaction, ceremony_id)?
544            .ok_or(FrostStoreError::Conflict("ceremony is absent"))?;
545        if stored.state != FrostCeremonyState::Completed {
546            return Err(FrostStoreError::Conflict("ceremony is not complete"));
547        }
548        let secret = decrypt_secret(&stored, custody)?;
549        transaction.commit().map_err(super::sqlite_error)?;
550        Ok(secret)
551    }
552}
553
554pub(super) fn verify_ceremony_invariants(connection: &Connection) -> Result<(), FrostStoreError> {
555    let mut statement = connection
556        .prepare("SELECT ceremony_id FROM frost_ceremonies ORDER BY ceremony_id")
557        .map_err(super::sqlite_error)?;
558    let ids = statement
559        .query_map([], |row| row.get::<_, String>(0))
560        .map_err(super::sqlite_error)?
561        .collect::<Result<Vec<_>, _>>()
562        .map_err(super::sqlite_error)?;
563    drop(statement);
564    for ceremony_id in ids {
565        let stored = load_ceremony_connection(connection, &ceremony_id)?
566            .ok_or_else(|| invalid("ceremony disappeared during invariant verification"))?;
567        verify_stored_row(&stored)?;
568        let latest = connection
569            .query_row(
570                r#"
571                SELECT projection_sequence, record_digest
572                FROM frost_projection_commits
573                WHERE projection_key = ?1
574                ORDER BY projection_sequence DESC LIMIT 1
575                "#,
576                [projection_key(&ceremony_id)],
577                |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
578            )
579            .optional()
580            .map_err(super::sqlite_error)?
581            .ok_or_else(|| invalid("ceremony has no projection commit"))?;
582        if read_u64(latest.0, "projection sequence")? != stored.state_version
583            || latest.1 != stored.record_digest
584        {
585            return Err(invalid(
586                "ceremony does not match its latest projection commit",
587            ));
588        }
589    }
590    verify_projection_commit_chains(connection)
591}
592
593fn insert_ceremony(
594    transaction: &Transaction<'_>,
595    write: &CeremonyWrite<'_>,
596    digest: &str,
597) -> Result<(), FrostStoreError> {
598    let changed = transaction
599        .execute(
600            r#"
601            INSERT INTO frost_ceremonies (
602                ceremony_id, config_json, config_digest, participant_set_digest,
603                scope_id, key_epoch, local_participant_id, state, state_version,
604                custody_generation, secret_kind, secret_nonce, secret_ciphertext,
605                output_nonce, output_ciphertext, input_transcript_digest,
606                public_key_package, group_public_key, verification_shares_json,
607                source_store_uuid, source_lease_id, source_owner_epoch,
608                updated_at_unix_ms, completed_at_unix_ms, record_digest
609            ) VALUES (
610                ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10,
611                ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19,
612                ?20, ?21, ?22, ?23, ?24, ?25
613            )
614            "#,
615            rusqlite::params_from_iter(ceremony_param_values(write, digest)?),
616        )
617        .map_err(super::sqlite_error)?;
618    if changed != 1 {
619        return Err(FrostStoreError::Conflict("ceremony insert lost its CAS"));
620    }
621    Ok(())
622}
623
624fn update_ceremony(
625    transaction: &Transaction<'_>,
626    write: &CeremonyWrite<'_>,
627    digest: &str,
628    expected_version: u64,
629) -> Result<(), FrostStoreError> {
630    let changed = transaction
631        .execute(
632            r#"
633            UPDATE frost_ceremonies SET
634                config_json = ?2, config_digest = ?3, participant_set_digest = ?4,
635                scope_id = ?5, key_epoch = ?6, local_participant_id = ?7,
636                state = ?8, state_version = ?9, custody_generation = ?10,
637                secret_kind = ?11, secret_nonce = ?12, secret_ciphertext = ?13,
638                output_nonce = ?14, output_ciphertext = ?15,
639                input_transcript_digest = ?16, public_key_package = ?17,
640                group_public_key = ?18, verification_shares_json = ?19,
641                source_store_uuid = ?20, source_lease_id = ?21,
642                source_owner_epoch = ?22, updated_at_unix_ms = ?23,
643                completed_at_unix_ms = ?24, record_digest = ?25
644            WHERE ceremony_id = ?1 AND state_version = ?26
645            "#,
646            rusqlite::params_from_iter(ceremony_param_values(write, digest)?.into_iter().chain([
647                rusqlite::types::Value::Integer(sqlite_u64(
648                    expected_version,
649                    "expected ceremony version",
650                )?),
651            ])),
652        )
653        .map_err(super::sqlite_error)?;
654    if changed != 1 {
655        return Err(FrostStoreError::Fenced);
656    }
657    Ok(())
658}
659
660fn ceremony_param_values(
661    write: &CeremonyWrite<'_>,
662    digest: &str,
663) -> Result<Vec<rusqlite::types::Value>, FrostStoreError> {
664    use rusqlite::types::Value;
665    let completed_at = match write.completed_at_unix_ms {
666        Some(value) => Value::Integer(sqlite_u64(value, "ceremony completion time")?),
667        None => Value::Null,
668    };
669    Ok(vec![
670        Value::Text(write.ceremony_id.to_string()),
671        Value::Blob(write.config_json.to_vec()),
672        Value::Text(write.config_digest.to_string()),
673        Value::Text(write.participant_set_digest.to_string()),
674        Value::Text(write.scope_id.to_string()),
675        Value::Integer(sqlite_u64(write.key_epoch, "ceremony key epoch")?),
676        Value::Text(write.local_participant_id.to_string()),
677        Value::Text(write.state.as_str().to_string()),
678        Value::Integer(sqlite_u64(write.state_version, "ceremony state version")?),
679        Value::Text(write.custody_generation.to_string()),
680        Value::Text(write.secret_kind.to_string()),
681        Value::Blob(write.secret.nonce.to_vec()),
682        Value::Blob(write.secret.ciphertext.clone()),
683        write
684            .output
685            .map_or(Value::Null, |output| Value::Blob(output.nonce.to_vec())),
686        write
687            .output
688            .map_or(Value::Null, |output| Value::Blob(output.ciphertext.clone())),
689        write
690            .input_transcript_digest
691            .map_or(Value::Null, |value| Value::Text(value.to_string())),
692        write
693            .public_key_package
694            .map_or(Value::Null, |value| Value::Blob(value.to_vec())),
695        write
696            .group_public_key
697            .map_or(Value::Null, |value| Value::Text(value.to_string())),
698        write
699            .verification_shares_json
700            .map_or(Value::Null, |value| Value::Blob(value.to_vec())),
701        Value::Text(write.source_fence.store_uuid.clone()),
702        Value::Text(write.source_fence.lease_id.clone()),
703        Value::Integer(sqlite_u64(
704            write.source_fence.owner_epoch,
705            "ceremony source owner epoch",
706        )?),
707        Value::Integer(sqlite_u64(
708            write.updated_at_unix_ms,
709            "ceremony updated time",
710        )?),
711        completed_at,
712        Value::Text(digest.to_string()),
713    ])
714}
715
716fn load_ceremony_tx(
717    transaction: &Transaction<'_>,
718    ceremony_id: &str,
719) -> Result<Option<StoredCeremonyRow>, FrostStoreError> {
720    load_ceremony_query(transaction, ceremony_id)
721}
722
723fn load_ceremony_connection(
724    connection: &Connection,
725    ceremony_id: &str,
726) -> Result<Option<StoredCeremonyRow>, FrostStoreError> {
727    load_ceremony_query(connection, ceremony_id)
728}
729
730fn load_ceremony_query(
731    connection: &Connection,
732    ceremony_id: &str,
733) -> Result<Option<StoredCeremonyRow>, FrostStoreError> {
734    let row = connection
735        .query_row(
736            r#"
737            SELECT ceremony_id, config_json, config_digest, participant_set_digest,
738                   scope_id, key_epoch, local_participant_id, state, state_version,
739                   custody_generation, secret_kind, secret_nonce, secret_ciphertext,
740                   output_nonce, output_ciphertext, input_transcript_digest,
741                   public_key_package, group_public_key, verification_shares_json,
742                   source_store_uuid, source_lease_id, source_owner_epoch,
743                   updated_at_unix_ms, completed_at_unix_ms, record_digest
744            FROM frost_ceremonies WHERE ceremony_id = ?1
745            "#,
746            [ceremony_id],
747            decode_row,
748        )
749        .optional()
750        .map_err(super::sqlite_error)?;
751    if let Some(stored) = row.as_ref() {
752        verify_stored_row(stored)?;
753    }
754    Ok(row)
755}
756
757fn decode_row(row: &Row<'_>) -> Result<StoredCeremonyRow, rusqlite::Error> {
758    let secret_nonce = nonce_from_row(row, 11)?;
759    let output_nonce = row
760        .get::<_, Option<Vec<u8>>>(13)?
761        .map(|bytes| nonce_from_bytes(bytes, 13))
762        .transpose()?;
763    let output_ciphertext = row.get::<_, Option<Vec<u8>>>(14)?;
764    let output = match (output_nonce, output_ciphertext) {
765        (Some(nonce), Some(ciphertext)) => Some(EncryptedBlob { nonce, ciphertext }),
766        (None, None) => None,
767        _ => {
768            return Err(rusqlite::Error::InvalidColumnType(
769                13,
770                "output_nonce".to_string(),
771                rusqlite::types::Type::Null,
772            ));
773        }
774    };
775    Ok(StoredCeremonyRow {
776        ceremony_id: row.get(0)?,
777        config_json: row.get(1)?,
778        config_digest: row.get(2)?,
779        participant_set_digest: row.get(3)?,
780        scope_id: row.get(4)?,
781        key_epoch: sqlite_read_u64(row, 5)?,
782        local_participant_id: row.get(6)?,
783        state: FrostCeremonyState::parse(&row.get::<_, String>(7)?)
784            .map_err(|error| rusqlite::Error::InvalidParameterName(error.to_string()))?,
785        state_version: sqlite_read_u64(row, 8)?,
786        custody_generation: row.get(9)?,
787        secret_kind: row.get(10)?,
788        secret: EncryptedBlob {
789            nonce: secret_nonce,
790            ciphertext: row.get(12)?,
791        },
792        output,
793        input_transcript_digest: row.get(15)?,
794        public_key_package: row.get(16)?,
795        group_public_key: row.get(17)?,
796        verification_shares_json: row.get(18)?,
797        source_fence: StoreMutationFence {
798            store_uuid: row.get(19)?,
799            lease_id: row.get(20)?,
800            owner_epoch: sqlite_read_u64(row, 21)?,
801        },
802        updated_at_unix_ms: sqlite_read_u64(row, 22)?,
803        completed_at_unix_ms: row
804            .get::<_, Option<i64>>(23)?
805            .map(|value| {
806                u64::try_from(value)
807                    .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(23, value))
808            })
809            .transpose()?,
810        record_digest: row.get(24)?,
811    })
812}
813
814fn verify_stored_row(stored: &StoredCeremonyRow) -> Result<(), FrostStoreError> {
815    let config: FrostCeremonyConfig = serde_json::from_slice(&stored.config_json)
816        .map_err(|error| invalid(format!("ceremony config does not decode: {error}")))?;
817    config.validate()?;
818    if config.ceremony_id()? != stored.ceremony_id
819        || sha256_hex(&stored.config_json) != stored.config_digest
820        || config.participant_set_digest()? != stored.participant_set_digest
821        || config.scope_id != stored.scope_id
822        || config.key_epoch != stored.key_epoch
823        || config.local_participant_id != stored.local_participant_id
824    {
825        return Err(invalid("ceremony config binding is invalid"));
826    }
827    let expected_version = match stored.state {
828        FrostCeremonyState::Round1Ready => 1,
829        FrostCeremonyState::Round2Ready => 2,
830        FrostCeremonyState::Completed => 3,
831    };
832    if stored.state_version != expected_version {
833        return Err(invalid("ceremony state version is invalid"));
834    }
835    let write = row_as_write(stored);
836    if record_digest(&write)? != stored.record_digest {
837        return Err(invalid("ceremony record digest does not match"));
838    }
839    Ok(())
840}
841
842fn verify_exact_config(
843    stored: &StoredCeremonyRow,
844    config_digest: &str,
845    custody: &FrostCustodyKey,
846    trusted_now_unix_ms: u64,
847) -> Result<(), FrostStoreError> {
848    if stored.config_digest != config_digest {
849        return Err(FrostStoreError::Conflict("ceremony configuration changed"));
850    }
851    if stored.custody_generation != custody.generation() {
852        return Err(FrostStoreError::Custody(
853            "custody generation does not match the ceremony",
854        ));
855    }
856    if trusted_now_unix_ms < stored.updated_at_unix_ms {
857        return Err(FrostStoreError::Conflict(
858            "trusted time regressed behind the ceremony",
859        ));
860    }
861    Ok(())
862}
863
864fn decrypt_secret(
865    stored: &StoredCeremonyRow,
866    custody: &FrostCustodyKey,
867) -> Result<FrostCeremonySecret, FrostStoreError> {
868    verify_custody_generation(stored, custody)?;
869    let plaintext = decrypt_blob_with_aad(
870        custody.key(),
871        &stored.secret,
872        &custody_aad("secret", &stored.custody_binding())?,
873    )
874    .map_err(|_| FrostStoreError::Custody("ceremony secret authentication failed"))?;
875    FrostCeremonySecret::from_custody_bytes(
876        parse_secret_kind(&stored.secret_kind)?,
877        Zeroizing::new(plaintext),
878    )
879    .map_err(Into::into)
880}
881
882fn decrypt_output(
883    stored: &StoredCeremonyRow,
884    custody: &FrostCustodyKey,
885) -> Result<StoredCeremonyOutput, FrostStoreError> {
886    verify_custody_generation(stored, custody)?;
887    let output = stored
888        .output
889        .as_ref()
890        .ok_or_else(|| invalid("ceremony output is absent"))?;
891    let plaintext = Zeroizing::new(
892        decrypt_blob_with_aad(
893            custody.key(),
894            output,
895            &custody_aad("output", &stored.custody_binding())?,
896        )
897        .map_err(|_| FrostStoreError::Custody("ceremony output authentication failed"))?,
898    );
899    serde_json::from_slice(&plaintext)
900        .map_err(|error| invalid(format!("ceremony output does not decode: {error}")))
901}
902
903fn completion_from_row(
904    stored: &StoredCeremonyRow,
905) -> Result<StoredFrostCeremonyCompletion, FrostStoreError> {
906    let verification_shares = serde_json::from_slice::<BTreeMap<String, String>>(
907        stored
908            .verification_shares_json
909            .as_deref()
910            .ok_or_else(|| invalid("completed ceremony lacks verification shares"))?,
911    )
912    .map_err(|error| invalid(format!("verification shares do not decode: {error}")))?;
913    Ok(StoredFrostCeremonyCompletion {
914        ceremony_id: stored.ceremony_id.clone(),
915        state: stored.state,
916        state_version: stored.state_version,
917        public_key_package: stored
918            .public_key_package
919            .clone()
920            .ok_or_else(|| invalid("completed ceremony lacks a public key package"))?,
921        group_public_key: stored
922            .group_public_key
923            .clone()
924            .ok_or_else(|| invalid("completed ceremony lacks a group public key"))?,
925        verification_shares,
926        transcript_digest: stored
927            .input_transcript_digest
928            .clone()
929            .ok_or_else(|| invalid("completed ceremony lacks a transcript digest"))?,
930    })
931}
932
933fn public_record(stored: &StoredCeremonyRow) -> FrostCeremonyRecord {
934    FrostCeremonyRecord {
935        ceremony_id: stored.ceremony_id.clone(),
936        state: stored.state,
937        state_version: stored.state_version,
938        participant_set_digest: stored.participant_set_digest.clone(),
939        scope_id: stored.scope_id.clone(),
940        key_epoch: stored.key_epoch,
941        local_participant_id: stored.local_participant_id.clone(),
942        input_transcript_digest: stored.input_transcript_digest.clone(),
943    }
944}
945
946fn row_as_write(stored: &StoredCeremonyRow) -> CeremonyWrite<'_> {
947    CeremonyWrite {
948        ceremony_id: &stored.ceremony_id,
949        config_json: &stored.config_json,
950        config_digest: &stored.config_digest,
951        participant_set_digest: &stored.participant_set_digest,
952        scope_id: &stored.scope_id,
953        key_epoch: stored.key_epoch,
954        local_participant_id: &stored.local_participant_id,
955        state: stored.state,
956        state_version: stored.state_version,
957        custody_generation: &stored.custody_generation,
958        secret_kind: &stored.secret_kind,
959        secret: &stored.secret,
960        output: stored.output.as_ref(),
961        input_transcript_digest: stored.input_transcript_digest.as_deref(),
962        public_key_package: stored.public_key_package.as_deref(),
963        group_public_key: stored.group_public_key.as_deref(),
964        verification_shares_json: stored.verification_shares_json.as_deref(),
965        source_fence: &stored.source_fence,
966        updated_at_unix_ms: stored.updated_at_unix_ms,
967        completed_at_unix_ms: stored.completed_at_unix_ms,
968    }
969}
970
971fn record_digest(write: &CeremonyWrite<'_>) -> Result<String, FrostStoreError> {
972    let preimage = RecordDigestPreimage {
973        format: "chio.frost.ceremony-record.v1",
974        ceremony_id: write.ceremony_id,
975        config_digest: write.config_digest,
976        participant_set_digest: write.participant_set_digest,
977        scope_id: write.scope_id,
978        key_epoch: write.key_epoch,
979        local_participant_id: write.local_participant_id,
980        state: write.state,
981        state_version: write.state_version,
982        custody_generation: write.custody_generation,
983        secret_kind: write.secret_kind,
984        secret_nonce: hex::encode(write.secret.nonce),
985        secret_ciphertext_digest: sha256_hex(&write.secret.ciphertext),
986        output_nonce: write.output.map(|output| hex::encode(output.nonce)),
987        output_ciphertext_digest: write.output.map(|output| sha256_hex(&output.ciphertext)),
988        input_transcript_digest: write.input_transcript_digest,
989        public_key_package_digest: write.public_key_package.map(sha256_hex),
990        group_public_key: write.group_public_key,
991        verification_shares_digest: write.verification_shares_json.map(sha256_hex),
992        store_uuid: &write.source_fence.store_uuid,
993        store_lease_id: &write.source_fence.lease_id,
994        store_owner_epoch: write.source_fence.owner_epoch,
995        updated_at_unix_ms: write.updated_at_unix_ms,
996        completed_at_unix_ms: write.completed_at_unix_ms,
997    };
998    prefixed_digest(RECORD_DIGEST_PREFIX, &preimage)
999}
1000
1001fn custody_aad(purpose: &str, binding: &CustodyBinding<'_>) -> Result<Vec<u8>, FrostStoreError> {
1002    canonical_json_bytes(&CustodyAad {
1003        format: CUSTODY_AAD_FORMAT,
1004        purpose,
1005        ceremony_id: binding.ceremony_id,
1006        config_digest: binding.config_digest,
1007        state: binding.state,
1008        state_version: binding.state_version,
1009        custody_generation: binding.custody_generation,
1010        store_uuid: &binding.fence.store_uuid,
1011        store_lease_id: &binding.fence.lease_id,
1012        store_owner_epoch: binding.fence.owner_epoch,
1013        updated_at_unix_ms: binding.updated_at_unix_ms,
1014    })
1015    .map_err(|error| FrostStoreError::InvalidState(error.to_string()))
1016}
1017
1018fn encrypt_material(
1019    plaintext: &[u8],
1020    custody: &FrostCustodyKey,
1021    aad: &[u8],
1022) -> Result<EncryptedBlob, FrostStoreError> {
1023    try_encrypt_blob_with_aad(custody.key(), plaintext, aad)
1024        .map_err(|_| FrostStoreError::Custody("ceremony material encryption failed"))
1025}
1026
1027fn verify_custody_generation(
1028    stored: &StoredCeremonyRow,
1029    custody: &FrostCustodyKey,
1030) -> Result<(), FrostStoreError> {
1031    if stored.custody_generation != custody.generation() {
1032        return Err(FrostStoreError::Custody(
1033            "custody generation does not match the ceremony",
1034        ));
1035    }
1036    Ok(())
1037}
1038
1039fn parse_secret_kind(value: &str) -> Result<FrostCeremonySecretKind, FrostStoreError> {
1040    match value {
1041        "round1" => Ok(FrostCeremonySecretKind::Round1),
1042        "round2" => Ok(FrostCeremonySecretKind::Round2),
1043        "key_package" => Ok(FrostCeremonySecretKind::KeyPackage),
1044        _ => Err(invalid("ceremony secret kind is unknown")),
1045    }
1046}
1047
1048fn projection_key(ceremony_id: &str) -> String {
1049    format!("ceremony/{ceremony_id}")
1050}
1051
1052fn validate_trusted_time(value: u64) -> Result<(), FrostStoreError> {
1053    if value == 0 || i64::try_from(value).is_err() {
1054        return Err(FrostStoreError::Conflict(
1055            "trusted time is outside the SQLite range",
1056        ));
1057    }
1058    Ok(())
1059}
1060
1061fn nonce_from_row(row: &Row<'_>, index: usize) -> Result<[u8; 12], rusqlite::Error> {
1062    nonce_from_bytes(row.get(index)?, index)
1063}
1064
1065fn nonce_from_bytes(bytes: Vec<u8>, index: usize) -> Result<[u8; 12], rusqlite::Error> {
1066    let length = bytes.len();
1067    bytes.try_into().map_err(|_| {
1068        rusqlite::Error::FromSqlConversionFailure(
1069            index,
1070            rusqlite::types::Type::Blob,
1071            format!("FROST nonce must be 12 bytes, got {length}").into(),
1072        )
1073    })
1074}
1075
1076fn sqlite_read_u64(row: &Row<'_>, index: usize) -> Result<u64, rusqlite::Error> {
1077    let value = row.get::<_, i64>(index)?;
1078    u64::try_from(value).map_err(|_| rusqlite::Error::IntegralValueOutOfRange(index, value))
1079}
1080
1081fn sqlite_u64(value: u64, field: &'static str) -> Result<i64, FrostStoreError> {
1082    i64::try_from(value)
1083        .map_err(|_| FrostStoreError::InvalidState(format!("{field} exceeds SQLite range")))
1084}
1085
1086fn read_u64(value: i64, field: &'static str) -> Result<u64, FrostStoreError> {
1087    u64::try_from(value).map_err(|_| FrostStoreError::InvalidState(format!("{field} is negative")))
1088}
1089
1090fn invalid(detail: impl Into<String>) -> FrostStoreError {
1091    FrostStoreError::InvalidState(detail.into())
1092}