chio-store-sqlite 0.1.2

SQLite-backed persistence, query, and report implementations for Chio
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use chio_core::Keypair;
use chio_federation_authority::{
    advance_frost_ceremony, begin_frost_ceremony, FrostCeremonyConfig, FrostCeremonyParticipant,
};
use chio_store_sqlite::{
    FrostCeremonyState, FrostCustodyKey, SqliteAuthorityStore, SqliteFrostStore,
};
use rand_chacha::ChaCha20Rng;
use rand_core::SeedableRng;
use tempfile::TempDir;

struct StoreFixture {
    _temp: TempDir,
    database: PathBuf,
    lock_root: PathBuf,
}

impl StoreFixture {
    fn new() -> Self {
        let temp = tempfile::tempdir().unwrap_or_else(|error| panic!("tempdir: {error}"));
        let database = temp.path().join("authority.db");
        let lock_root = temp.path().join("locks");
        fs::create_dir(&lock_root).unwrap_or_else(|error| panic!("create lock root: {error}"));
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o700))
                .unwrap_or_else(|error| panic!("secure database parent: {error}"));
            fs::set_permissions(&lock_root, fs::Permissions::from_mode(0o700))
                .unwrap_or_else(|error| panic!("secure lock root: {error}"));
        }
        SqliteAuthorityStore::provision(&database, &lock_root)
            .unwrap_or_else(|error| panic!("provision authority: {error}"));
        Self {
            _temp: temp,
            database,
            lock_root,
        }
    }

    fn open(&self) -> (SqliteAuthorityStore, SqliteFrostStore) {
        let authority = SqliteAuthorityStore::open_serving(&self.database, &self.lock_root)
            .unwrap_or_else(|error| panic!("open authority: {error}"));
        let frost = authority.frost_store();
        (authority, frost)
    }
}

struct ParticipantFixture {
    config: FrostCeremonyConfig,
    transport_key: Keypair,
}

fn participants() -> Vec<ParticipantFixture> {
    let keys = [
        Keypair::from_seed(&[0x51; 32]),
        Keypair::from_seed(&[0x52; 32]),
        Keypair::from_seed(&[0x53; 32]),
    ];
    let participants = keys
        .iter()
        .enumerate()
        .map(|(index, key)| FrostCeremonyParticipant {
            participant_id: format!("operator-{}", index + 1),
            transport_key_id: format!("operator-{}.dkg.v1", index + 1),
            transport_public_key: key.public_key(),
        })
        .collect::<Vec<_>>();
    keys.into_iter()
        .enumerate()
        .map(|(index, transport_key)| ParticipantFixture {
            config: FrostCeremonyConfig {
                scope_id: "treaty.atlantic.v1".to_string(),
                key_epoch: 2,
                threshold: 2,
                predecessor_roster_digest: Some("64".repeat(32)),
                participants: participants.clone(),
                local_participant_id: format!("operator-{}", index + 1),
            },
            transport_key,
        })
        .collect()
}

fn custody() -> FrostCustodyKey {
    FrostCustodyKey::new("custody-generation-7", [0xa5; 32])
        .unwrap_or_else(|error| panic!("custody key: {error}"))
}

fn reopen(
    fixture: &StoreFixture,
    authority: SqliteAuthorityStore,
    frost: SqliteFrostStore,
) -> (SqliteAuthorityStore, SqliteFrostStore) {
    drop(frost);
    drop(authority);
    fixture.open()
}

#[test]
fn frost_rotation_ceremony_commits_each_round_before_replay_after_restart() {
    let fixture = StoreFixture::new();
    let participants = participants();
    let (mut authority, mut frost) = fixture.open();

    let mut rng = ChaCha20Rng::from_seed([1; 32]);
    let first = frost
        .begin_ceremony(
            &participants[0].config,
            &participants[0].transport_key,
            &custody(),
            &mut rng,
            &authority.mutation_fence(),
            1_000,
        )
        .unwrap_or_else(|error| panic!("persist round one: {error}"));
    assert_eq!(first.state, FrostCeremonyState::Round1Ready);

    (authority, frost) = reopen(&fixture, authority, frost);
    let mut retry_rng = ChaCha20Rng::from_seed([99; 32]);
    let replayed_first = frost
        .begin_ceremony(
            &participants[0].config,
            &participants[0].transport_key,
            &custody(),
            &mut retry_rng,
            &authority.mutation_fence(),
            1_001,
        )
        .unwrap_or_else(|error| panic!("replay round one: {error}"));
    assert_eq!(replayed_first.package, first.package);
    assert_eq!(replayed_first.state_version, first.state_version);

    let mut round1 = vec![replayed_first.package.clone()];
    let mut peer_round1_secrets = Vec::new();
    for (index, participant) in participants.iter().enumerate().skip(1) {
        let mut rng = ChaCha20Rng::from_seed([index as u8 + 1; 32]);
        let transition =
            begin_frost_ceremony(&participant.config, &participant.transport_key, &mut rng)
                .unwrap_or_else(|error| panic!("peer round one: {error}"));
        round1.push(transition.package);
        peer_round1_secrets.push(transition.secret);
    }

    let second = frost
        .advance_ceremony(
            &participants[0].config,
            &participants[0].transport_key,
            &custody(),
            &round1,
            &authority.mutation_fence(),
            2_000,
        )
        .unwrap_or_else(|error| panic!("persist round two: {error}"));
    assert_eq!(second.state, FrostCeremonyState::Round2Ready);

    (authority, frost) = reopen(&fixture, authority, frost);
    let replayed_second = frost
        .advance_ceremony(
            &participants[0].config,
            &participants[0].transport_key,
            &custody(),
            &round1,
            &authority.mutation_fence(),
            2_001,
        )
        .unwrap_or_else(|error| panic!("replay round two: {error}"));
    assert_eq!(replayed_second.packages, second.packages);
    assert_eq!(replayed_second.state_version, second.state_version);

    let mut round2 = replayed_second.packages.clone();
    for ((participant, secret), index) in participants
        .iter()
        .skip(1)
        .zip(peer_round1_secrets)
        .zip(0_u8..)
    {
        let transition = advance_frost_ceremony(
            &participant.config,
            &participant.transport_key,
            secret,
            &round1,
        )
        .unwrap_or_else(|error| panic!("peer round two {index}: {error}"));
        round2.extend(transition.packages);
    }

    let completed = frost
        .complete_ceremony(
            &participants[0].config,
            &custody(),
            &round1,
            &round2,
            &authority.mutation_fence(),
            3_000,
        )
        .unwrap_or_else(|error| panic!("persist completion: {error}"));
    assert_eq!(completed.state, FrostCeremonyState::Completed);

    (authority, frost) = reopen(&fixture, authority, frost);
    let replayed_completion = frost
        .complete_ceremony(
            &participants[0].config,
            &custody(),
            &round1,
            &round2,
            &authority.mutation_fence(),
            3_001,
        )
        .unwrap_or_else(|error| panic!("replay completion: {error}"));
    assert_eq!(replayed_completion, completed);
    let key_package = frost
        .load_completed_key_package(
            &completed.ceremony_id,
            &custody(),
            &authority.mutation_fence(),
        )
        .unwrap_or_else(|error| panic!("load completed key: {error}"));
    assert_eq!(
        key_package.kind(),
        chio_federation_authority::FrostCeremonySecretKind::KeyPackage
    );
}

#[test]
fn frost_rotation_ceremony_rejects_changed_config_and_wrong_custody_generation() {
    let fixture = StoreFixture::new();
    let participants = participants();
    let (authority, frost) = fixture.open();
    let mut rng = ChaCha20Rng::from_seed([4; 32]);
    let first = frost
        .begin_ceremony(
            &participants[0].config,
            &participants[0].transport_key,
            &custody(),
            &mut rng,
            &authority.mutation_fence(),
            1_000,
        )
        .unwrap_or_else(|error| panic!("persist round one: {error}"));

    let wrong_custody = FrostCustodyKey::new("custody-generation-8", [0xa5; 32])
        .unwrap_or_else(|error| panic!("wrong custody fixture: {error}"));
    assert!(frost
        .load_ceremony(&first.ceremony_id, &wrong_custody)
        .is_err());

    let mut changed = participants[0].config.clone();
    changed.threshold = 3;
    let mut retry_rng = ChaCha20Rng::from_seed([5; 32]);
    assert!(frost
        .begin_ceremony(
            &changed,
            &participants[0].transport_key,
            &custody(),
            &mut retry_rng,
            &authority.mutation_fence(),
            1_001,
        )
        .is_err());
}

#[test]
fn frost_rotation_ceremony_never_persists_round_material_in_plaintext() {
    let fixture = StoreFixture::new();
    let participants = participants();
    let (authority, frost) = fixture.open();
    let mut rng = ChaCha20Rng::from_seed([6; 32]);
    let first = frost
        .begin_ceremony(
            &participants[0].config,
            &participants[0].transport_key,
            &custody(),
            &mut rng,
            &authority.mutation_fence(),
            1_000,
        )
        .unwrap_or_else(|error| panic!("persist round one: {error}"));
    let package_bytes = serde_json::to_vec(&first.package)
        .unwrap_or_else(|error| panic!("serialize package: {error}"));
    drop(frost);
    drop(authority);

    assert_database_files_exclude(&fixture.database, &package_bytes);
}

fn assert_database_files_exclude(database: &Path, needle: &[u8]) {
    for path in [
        database.to_path_buf(),
        PathBuf::from(format!("{}-wal", database.display())),
    ] {
        if let Ok(bytes) = fs::read(&path) {
            assert!(
                !bytes.windows(needle.len()).any(|window| window == needle),
                "{} contains plaintext ceremony output",
                path.display()
            );
        }
    }
}