chio-store-sqlite 0.1.2

SQLite-backed persistence, query, and report implementations for Chio
Documentation
use chio_core::{sha256_hex, StoreMutationFence};
use rusqlite::{params, Connection, OptionalExtension, Row, Transaction};

use super::{
    invalid, signer_projection_key, sqlite_read_u64, sqlite_u64, to_sql_conversion,
    SignerRecordPreimage, StoredSigner, SIGNER_RECORD_PREFIX,
};
use crate::encrypted_blob::EncryptedBlob;
use crate::frost_store::commit::{append_projection_commit, prefixed_digest, ProjectionMutation};
use crate::frost_store::{FrostSignerSessionState, FrostStoreError, SqliteFrostStore};

pub(super) fn insert_signer(
    transaction: &Transaction<'_>,
    stored: &StoredSigner,
) -> Result<(), FrostStoreError> {
    let nonce = stored
        .nonce
        .as_ref()
        .ok_or_else(|| invalid("prepared signer lacks encrypted nonce"))?;
    transaction
        .execute(
            r#"
            INSERT INTO frost_signer_sessions (
                session_id, participant_id, authorization_slot_id, scope_id,
                key_epoch, authorization_id, signing_message_digest, roster_digest,
                coordinator_id, resource_fence, ceremony_id, authorization_body_json,
                bound_checkpoint_digest, bound_checkpoint_json, signer_identifier,
                verification_share, state, state_version, custody_generation, nonce_aad,
                nonce_ciphertext, nonce_nonce, commitment_bytes, commitment_digest,
                signing_package_digest, signature_share, burn_reason, source_store_uuid,
                source_lease_id, source_owner_epoch, created_at_unix_ms,
                updated_at_unix_ms, record_digest
            ) VALUES (
                ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11,
                ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21,
                ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31,
                ?32, ?33
            )
            "#,
            params![
                &stored.session_id,
                &stored.participant_id,
                &stored.authorization_slot_id,
                &stored.scope_id,
                sqlite_u64(stored.key_epoch, "signer key epoch")?,
                &stored.authorization_id,
                &stored.signing_message_digest,
                &stored.roster_digest,
                &stored.coordinator_id,
                sqlite_u64(stored.resource_fence, "signer resource fence")?,
                &stored.ceremony_id,
                &stored.authorization_body_json,
                &stored.bound_checkpoint_digest,
                &stored.bound_checkpoint_json,
                &stored.signer_identifier,
                &stored.verification_share,
                stored.state.as_str(),
                sqlite_u64(stored.state_version, "signer state version")?,
                &stored.custody_generation,
                &stored.nonce_aad,
                &nonce.ciphertext,
                nonce.nonce.as_slice(),
                &stored.commitment_bytes,
                &stored.commitment_digest,
                stored.signing_package_digest.as_deref(),
                stored.signature_share.as_deref(),
                stored.burn_reason.as_deref(),
                &stored.source_fence.store_uuid,
                &stored.source_fence.lease_id,
                sqlite_u64(stored.source_fence.owner_epoch, "signer owner epoch")?,
                sqlite_u64(stored.created_at_unix_ms, "signer created time")?,
                sqlite_u64(stored.updated_at_unix_ms, "signer updated time")?,
                &stored.record_digest,
            ],
        )
        .map_err(crate::frost_store::sqlite_error)?;
    Ok(())
}

pub(super) fn update_signer(
    transaction: &Transaction<'_>,
    stored: &StoredSigner,
    previous_state: FrostSignerSessionState,
    previous_version: u64,
) -> Result<(), FrostStoreError> {
    let changed = transaction
        .execute(
            r#"
            UPDATE frost_signer_sessions
            SET state = ?1, state_version = ?2, nonce_ciphertext = ?3,
                nonce_nonce = ?4, signing_package_digest = ?5,
                signature_share = ?6, burn_reason = ?7, source_store_uuid = ?8,
                source_lease_id = ?9, source_owner_epoch = ?10,
                updated_at_unix_ms = ?11, record_digest = ?12
            WHERE session_id = ?13 AND participant_id = ?14
              AND state = ?15 AND state_version = ?16
            "#,
            params![
                stored.state.as_str(),
                sqlite_u64(stored.state_version, "signer state version")?,
                stored
                    .nonce
                    .as_ref()
                    .map(|nonce| nonce.ciphertext.as_slice()),
                stored.nonce.as_ref().map(|nonce| nonce.nonce.as_slice()),
                stored.signing_package_digest.as_deref(),
                stored.signature_share.as_deref(),
                stored.burn_reason.as_deref(),
                &stored.source_fence.store_uuid,
                &stored.source_fence.lease_id,
                sqlite_u64(stored.source_fence.owner_epoch, "signer owner epoch")?,
                sqlite_u64(stored.updated_at_unix_ms, "signer updated time")?,
                &stored.record_digest,
                &stored.session_id,
                &stored.participant_id,
                previous_state.as_str(),
                sqlite_u64(previous_version, "previous signer state version")?,
            ],
        )
        .map_err(crate::frost_store::sqlite_error)?;
    if changed != 1 {
        return Err(FrostStoreError::Conflict(
            "signer session changed during transition",
        ));
    }
    Ok(())
}

pub(super) fn append_signer_commit(
    transaction: &Transaction<'_>,
    store: &SqliteFrostStore,
    stored: &StoredSigner,
    mutation_kind: &str,
    fence: &StoreMutationFence,
) -> Result<(), FrostStoreError> {
    append_projection_commit(
        transaction,
        store,
        &signer_projection_key(&stored.session_id, &stored.participant_id),
        ProjectionMutation {
            sequence: stored.state_version,
            projection_type: "signer",
            mutation_kind,
            record_digest: &stored.record_digest,
        },
        fence,
    )
}

pub(super) fn load_signer_by_slot_query(
    connection: &Connection,
    slot_id: &str,
    participant_id: &str,
) -> Result<Option<StoredSigner>, FrostStoreError> {
    connection
        .query_row(
            &format!(
                "{} WHERE authorization_slot_id = ?1 AND participant_id = ?2",
                signer_select()
            ),
            params![slot_id, participant_id],
            stored_signer_from_row,
        )
        .optional()
        .map_err(crate::frost_store::sqlite_error)
}

pub(super) fn load_signer_query(
    connection: &Connection,
    session_id: &str,
    participant_id: &str,
) -> Result<Option<StoredSigner>, FrostStoreError> {
    connection
        .query_row(
            &format!(
                "{} WHERE session_id = ?1 AND participant_id = ?2",
                signer_select()
            ),
            params![session_id, participant_id],
            stored_signer_from_row,
        )
        .optional()
        .map_err(crate::frost_store::sqlite_error)
}

fn signer_select() -> &'static str {
    r#"
    SELECT session_id, participant_id, authorization_slot_id, scope_id,
           key_epoch, authorization_id, signing_message_digest, roster_digest,
           coordinator_id, resource_fence, ceremony_id, authorization_body_json,
           bound_checkpoint_digest, bound_checkpoint_json, signer_identifier,
           verification_share, state, state_version, custody_generation, nonce_aad,
           nonce_ciphertext, nonce_nonce, commitment_bytes, commitment_digest,
           signing_package_digest, signature_share, burn_reason, source_store_uuid,
           source_lease_id, source_owner_epoch, created_at_unix_ms,
           updated_at_unix_ms, record_digest
    FROM frost_signer_sessions
    "#
}

fn stored_signer_from_row(row: &Row<'_>) -> rusqlite::Result<StoredSigner> {
    let nonce_ciphertext = row.get::<_, Option<Vec<u8>>>(20)?;
    let nonce_bytes = row.get::<_, Option<Vec<u8>>>(21)?;
    let nonce = match (nonce_ciphertext, nonce_bytes) {
        (Some(ciphertext), Some(bytes)) => {
            let nonce: [u8; 12] = bytes.as_slice().try_into().map_err(|_| {
                rusqlite::Error::FromSqlConversionFailure(
                    21,
                    rusqlite::types::Type::Blob,
                    "invalid FROST signer nonce length".into(),
                )
            })?;
            Some(EncryptedBlob { ciphertext, nonce })
        }
        (None, None) => None,
        _ => {
            return Err(rusqlite::Error::FromSqlConversionFailure(
                20,
                rusqlite::types::Type::Blob,
                "partial FROST signer nonce".into(),
            ))
        }
    };
    let state = FrostSignerSessionState::parse(row.get::<_, String>(16)?.as_str())
        .map_err(to_sql_conversion)?;
    Ok(StoredSigner {
        session_id: row.get(0)?,
        participant_id: row.get(1)?,
        authorization_slot_id: row.get(2)?,
        scope_id: row.get(3)?,
        key_epoch: sqlite_read_u64(row, 4)?,
        authorization_id: row.get(5)?,
        signing_message_digest: row.get(6)?,
        roster_digest: row.get(7)?,
        coordinator_id: row.get(8)?,
        resource_fence: sqlite_read_u64(row, 9)?,
        ceremony_id: row.get(10)?,
        authorization_body_json: row.get(11)?,
        bound_checkpoint_digest: row.get(12)?,
        bound_checkpoint_json: row.get(13)?,
        signer_identifier: row.get(14)?,
        verification_share: row.get(15)?,
        state,
        state_version: sqlite_read_u64(row, 17)?,
        custody_generation: row.get(18)?,
        nonce_aad: row.get(19)?,
        nonce,
        commitment_bytes: row.get(22)?,
        commitment_digest: row.get(23)?,
        signing_package_digest: row.get(24)?,
        signature_share: row.get(25)?,
        burn_reason: row.get(26)?,
        source_fence: StoreMutationFence {
            store_uuid: row.get(27)?,
            lease_id: row.get(28)?,
            owner_epoch: sqlite_read_u64(row, 29)?,
        },
        created_at_unix_ms: sqlite_read_u64(row, 30)?,
        updated_at_unix_ms: sqlite_read_u64(row, 31)?,
        record_digest: row.get(32)?,
    })
}

pub(super) fn signer_record_digest(stored: &StoredSigner) -> Result<String, FrostStoreError> {
    prefixed_digest(
        SIGNER_RECORD_PREFIX,
        &SignerRecordPreimage {
            format: "chio.frost.signer-record.v1",
            session_id: &stored.session_id,
            participant_id: &stored.participant_id,
            authorization_slot_id: &stored.authorization_slot_id,
            scope_id: &stored.scope_id,
            key_epoch: stored.key_epoch,
            authorization_id: &stored.authorization_id,
            signing_message_digest: &stored.signing_message_digest,
            roster_digest: &stored.roster_digest,
            coordinator_id: &stored.coordinator_id,
            resource_fence: stored.resource_fence,
            ceremony_id: &stored.ceremony_id,
            authorization_body_json_digest: sha256_hex(&stored.authorization_body_json),
            bound_checkpoint_digest: &stored.bound_checkpoint_digest,
            bound_checkpoint_json_digest: sha256_hex(&stored.bound_checkpoint_json),
            signer_identifier: hex::encode(&stored.signer_identifier),
            verification_share: &stored.verification_share,
            state: stored.state,
            state_version: stored.state_version,
            custody_generation: &stored.custody_generation,
            nonce_aad_digest: sha256_hex(&stored.nonce_aad),
            nonce_ciphertext_digest: stored
                .nonce
                .as_ref()
                .map(|nonce| sha256_hex(&nonce.ciphertext)),
            nonce_nonce: stored.nonce.as_ref().map(|nonce| hex::encode(nonce.nonce)),
            commitment_digest: sha256_hex(&stored.commitment_bytes),
            signing_package_digest: stored.signing_package_digest.as_deref(),
            signature_share_digest: stored.signature_share.as_deref().map(sha256_hex),
            burn_reason: stored.burn_reason.as_deref(),
            source_store_uuid: &stored.source_fence.store_uuid,
            source_lease_id: &stored.source_fence.lease_id,
            source_owner_epoch: stored.source_fence.owner_epoch,
            created_at_unix_ms: stored.created_at_unix_ms,
            updated_at_unix_ms: stored.updated_at_unix_ms,
        },
    )
}