//! End-to-end coverage for the Shamir custody ceremonies. use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_server::{ actors::{ GlobalActors, vault::{Bootstrap, GetState, Seal, VaultState}, vault_coordinator::{ContributeBootstrap, ContributeUnseal, StartBootstrap}, }, crypto::{KeyCell, shamir}, db::{self, models::OperatorId, schema}, }; use diesel::{ExpressionMethods as _, QueryDsl}; use diesel_async::RunQueryDsl; /// Register `count` operator identities so committee members satisfy the /// foreign key from `operator` to `operator_identity`. async fn register_operators(db: &db::DatabasePool, count: usize) -> Vec { let mut conn = db.get().await.unwrap(); let mut ids = Vec::with_capacity(count); for index in 0..count { let pubkey = vec![u8::try_from(index).unwrap(); 32]; let id: OperatorId = diesel::insert_into(schema::operator_identity::table) .values((schema::operator_identity::public_key.eq(pubkey),)) .returning(schema::operator_identity::id) .get_result(&mut conn) .await .unwrap(); ids.push(id); } ids } async fn stored_share_count(db: &db::DatabasePool) -> i64 { let mut conn = db.get().await.unwrap(); schema::operator::table .count() .get_result(&mut conn) .await .unwrap() } async fn stored_threshold(db: &db::DatabasePool) -> Option { let mut conn = db.get().await.unwrap(); schema::arbiter_settings::table .select(schema::arbiter_settings::shamir_threshold) .first(&mut conn) .await .unwrap() } fn passphrase(seed: u8) -> SafeCell> { SafeCell::new(vec![seed; 16]) } /// The happy path: three operators bootstrap, and any two of them reopen the /// vault after it is sealed. #[tokio::test] #[test_log::test] async fn committee_of_three_unseals_with_two_passphrases() { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let operators = register_operators(&db, 3).await; actors .vault_coordinator .ask(StartBootstrap { operator_id: operators[0], declared_count: 3, }) .await .unwrap(); for (index, operator_id) in operators.iter().enumerate() { let finished = actors .vault_coordinator .ask(ContributeBootstrap { operator_id: *operator_id, passphrase: passphrase(u8::try_from(index).unwrap()), }) .await .unwrap(); assert_eq!( finished, index == 2, "the ceremony finishes only on the last contribution" ); } assert_eq!( actors.vault.ask(GetState {}).await.unwrap(), VaultState::Unsealed ); assert_eq!(stored_share_count(&db).await, 3); assert_eq!(stored_threshold(&db).await, Some(2)); actors.vault.ask(Seal {}).await.unwrap(); let first = actors .vault_coordinator .ask(ContributeUnseal { operator_id: operators[0], passphrase: passphrase(0), }) .await .unwrap(); assert!(!first, "one of two shares must not unseal"); let second = actors .vault_coordinator .ask(ContributeUnseal { operator_id: operators[2], passphrase: passphrase(2), }) .await .unwrap(); assert!(second, "the threshold contribution should unseal the vault"); assert_eq!( actors.vault.ask(GetState {}).await.unwrap(), VaultState::Unsealed ); } /// Shares that describe a seal key the vault never adopted would make the vault /// permanently un-unsealable, so a refused bootstrap must leave the table empty. #[tokio::test] #[test_log::test] async fn refused_bootstrap_stores_no_shares() { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let operators = register_operators(&db, 1).await; // Another path bootstraps the vault first; the ceremony now has nowhere to go. actors .vault .ask(Bootstrap { seal_key: KeyCell::from([4u8; 32]), custody: None, }) .await .unwrap(); actors .vault_coordinator .ask(StartBootstrap { operator_id: operators[0], declared_count: 1, }) .await .unwrap(); let error = actors .vault_coordinator .ask(ContributeBootstrap { operator_id: operators[0], passphrase: passphrase(1), }) .await .expect_err("bootstrapping an already bootstrapped vault must fail"); assert!( format!("{error:?}").contains("AlreadyBootstrapped"), "expected AlreadyBootstrapped, got {error:?}" ); assert_eq!( stored_share_count(&db).await, 0, "a refused bootstrap must not leave shares behind" ); assert_eq!( stored_threshold(&db).await, None, "a refused bootstrap must not leave a threshold behind" ); } #[tokio::test] #[test_log::test] async fn oversized_and_degenerate_committees_are_rejected() { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let operators = register_operators(&db, 1).await; for (count, expected) in [ (0_usize, "EmptyCommittee"), (2, "UnsupportedCommittee"), (shamir::MAX_COMMITTEE_SIZE + 1, "CommitteeTooLarge"), (usize::MAX, "CommitteeTooLarge"), ] { let error = actors .vault_coordinator .ask(StartBootstrap { operator_id: operators[0], declared_count: count, }) .await .expect_err("committee size must be rejected"); assert!( format!("{error:?}").contains(expected), "expected {expected} for count {count}, got {error:?}" ); } } /// A committee whose members never all show up would otherwise wedge the /// coordinator until restart. Only the operator that declared it may reset it. #[tokio::test] #[test_log::test] async fn only_the_declarer_may_restart_a_stalled_committee() { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let operators = register_operators(&db, 3).await; actors .vault_coordinator .ask(StartBootstrap { operator_id: operators[0], declared_count: 3, }) .await .unwrap(); actors .vault_coordinator .ask(ContributeBootstrap { operator_id: operators[0], passphrase: passphrase(0), }) .await .unwrap(); let error = actors .vault_coordinator .ask(StartBootstrap { operator_id: operators[1], declared_count: 3, }) .await .expect_err("a bystander must not reset someone else's ceremony"); assert!( format!("{error:?}").contains("AlreadyBootstrapping"), "expected AlreadyBootstrapping, got {error:?}" ); actors .vault_coordinator .ask(StartBootstrap { operator_id: operators[0], declared_count: 1, }) .await .expect("the declarer may restart the ceremony"); let finished = actors .vault_coordinator .ask(ContributeBootstrap { operator_id: operators[0], passphrase: passphrase(0), }) .await .unwrap(); assert!( finished, "the restarted one-operator ceremony should complete" ); assert_eq!(stored_share_count(&db).await, 1); } /// A wrong passphrase must not unseal, and the operator must be able to retry. #[tokio::test] #[test_log::test] async fn wrong_passphrase_is_rejected_and_retryable() { let db = db::create_test_pool().await; let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let operators = register_operators(&db, 1).await; actors .vault_coordinator .ask(StartBootstrap { operator_id: operators[0], declared_count: 1, }) .await .unwrap(); actors .vault_coordinator .ask(ContributeBootstrap { operator_id: operators[0], passphrase: passphrase(7), }) .await .unwrap(); actors.vault.ask(Seal {}).await.unwrap(); let error = actors .vault_coordinator .ask(ContributeUnseal { operator_id: operators[0], passphrase: passphrase(8), }) .await .expect_err("a wrong passphrase must not unseal"); assert!( format!("{error:?}").contains("InvalidPassphrase"), "expected InvalidPassphrase, got {error:?}" ); let unsealed = actors .vault_coordinator .ask(ContributeUnseal { operator_id: operators[0], passphrase: passphrase(7), }) .await .expect("the operator may retry with the correct passphrase"); assert!(unsealed, "the retry should unseal the vault"); }