//! Plumbing shared by the two ECDSA providers (cait-sith //! [`EcdsaSignatureProvider`](crate::providers::ecdsa::EcdsaSignatureProvider) and Damgård-et-al //! [`RobustEcdsaSignatureProvider`](crate::providers::robust_ecdsa::RobustEcdsaSignatureProvider), //! both over secp256k1). Both keep the same //! per-domain keyshare + presignature store; only the presignature payload `P` and the surrounding //! protocol differ, so the storage and per-domain scaffolding are generic over `P` here. use crate::assets::DistributedAssetStorage; use crate::db::SecretDB; use crate::network::MeshNetworkClient; use crate::primitives::ParticipantId; use crate::providers::{DomainKeyshare, HasParticipants}; use mpc_primitives::ReconstructionThreshold; use mpc_primitives::domain::DomainId; use near_time::Clock; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use threshold_signatures::ecdsa::{KeygenOutput, Secp256K1Sha256}; /// A stored presignature together with the participants that produced it, so the store can drop it /// once any of those participants goes offline. Generic over the presignature payload `P`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PresignOutputWithParticipants

{ pub presignature: P, pub participants: Vec, } impl

HasParticipants for PresignOutputWithParticipants

{ fn is_subset_of_active_participants(&self, active_participants: &[ParticipantId]) -> bool { self.participants .iter() .all(|p| active_participants.contains(p)) } } /// Per-domain presignature store, keyed on disk by `domain_id` under [`crate::db::DBCol::Presignature`]. #[derive(derive_more::Deref)] pub struct PresignatureStorage

(DistributedAssetStorage>) where P: Serialize + DeserializeOwned + Send + 'static; impl

PresignatureStorage

where P: Serialize + DeserializeOwned + Send + 'static, { pub fn new( clock: Clock, db: Arc, client: &Arc, domain_id: DomainId, ) -> anyhow::Result { Ok(Self(DistributedAssetStorage::< PresignOutputWithParticipants

, >::new( clock, db, crate::db::DBCol::Presignature, domain_id.0.to_be_bytes().to_vec(), client.my_participant_id(), |participants, presignature| { presignature.is_subset_of_active_participants(participants) }, active_participants_query(client), )?)) } } /// A domain's [`DomainKeyshare`] material plus a presignature store, which is runtime state the /// coordinator can't provide and so is built here. pub struct EcdsaKeyshare

where P: Serialize + DeserializeOwned + Send + 'static, { pub keygen_output: KeygenOutput, pub presignature_store: Arc>, pub reconstruction_threshold: ReconstructionThreshold, } // Manual `Clone` so callers don't need `P: Clone` — every field is `Clone` regardless of `P`. impl

Clone for EcdsaKeyshare

where P: Serialize + DeserializeOwned + Send + 'static, { fn clone(&self) -> Self { Self { keygen_output: self.keygen_output.clone(), presignature_store: self.presignature_store.clone(), reconstruction_threshold: self.reconstruction_threshold, } } } /// The "are all these participants still alive?" query both the presignature and triple stores use. pub fn active_participants_query( client: &Arc, ) -> Arc Vec + Send + Sync> { let network_client = client.clone(); Arc::new(move || network_client.all_alive_participant_ids()) } /// Attaches a freshly-created presignature store to each domain's [`DomainKeyshare`]. pub fn build_keyshares

( clock: &Clock, db: &Arc, client: &Arc, keyshares: HashMap>, ) -> anyhow::Result>> where P: Serialize + DeserializeOwned + Send + 'static, { let mut result = HashMap::new(); for (domain_id, keyshare) in keyshares { let presignature_store = Arc::new(PresignatureStorage::new( clock.clone(), db.clone(), client, domain_id, )?); result.insert( domain_id, EcdsaKeyshare { keygen_output: keyshare.keygen_output, presignature_store, reconstruction_threshold: keyshare.reconstruction_threshold, }, ); } Ok(result) } /// Looks up a domain's keyshare, cloning it out of the map. pub fn lookup_keyshare

( keyshares: &HashMap>, domain_id: DomainId, ) -> anyhow::Result> where P: Serialize + DeserializeOwned + Send + 'static, { keyshares .get(&domain_id) .cloned() .ok_or_else(|| anyhow::anyhow!("No keyshare for domain {:?}", domain_id)) } #[cfg(test)] #[expect(non_snake_case)] mod tests { use super::{DomainKeyshare, build_keyshares}; use crate::db::SecretDB; use crate::network::testing::run_test_clients; use crate::tests::into_participant_ids; use crate::tracking::testing::start_root_task_with_periodic_dump; use mpc_primitives::ReconstructionThreshold; use mpc_primitives::domain::DomainId; use near_time::Clock; use rand::SeedableRng; use std::collections::HashMap; use threshold_signatures::ecdsa::KeygenOutput; use threshold_signatures::frost_secp256k1::Secp256K1Sha256; use threshold_signatures::test_utils::{generate_participants, run_keygen}; fn dummy_keygen_output() -> KeygenOutput { let mut rng = rand::rngs::StdRng::from_seed([7u8; 32]); run_keygen::(&generate_participants(2), 2usize, &mut rng) .into_iter() .next() .unwrap() .1 } // Directly asserts the plumbing that the `reconstruction_thresholds` integration tests // only check indirectly (by running full multi-node signing rounds): each domain must keep // its OWN reconstruction threshold, never a single shared/governance value. #[tokio::test] async fn build_keyshares__should_pair_each_domain_with_its_own_reconstruction_threshold() { start_root_task_with_periodic_dump(async move { run_test_clients( into_participant_ids(&generate_participants(2)), |client, _channel_receiver| async move { // Given two domains configured with distinct reconstruction thresholds let low = DomainId(0); let high = DomainId(1); let low_threshold = ReconstructionThreshold::new(2); let high_threshold = ReconstructionThreshold::new(3); let keygen_output = dummy_keygen_output(); let keyshares = HashMap::from([ ( low, DomainKeyshare::new(keygen_output.clone(), low_threshold), ), (high, DomainKeyshare::new(keygen_output, high_threshold)), ]); let dir = tempfile::tempdir().unwrap(); let db = SecretDB::new(dir.path(), [1; 16]).unwrap(); // When let keyshares = build_keyshares::>(&Clock::real(), &db, &client, keyshares) .unwrap(); // Then each domain keeps the threshold it was configured with assert_eq!(keyshares[&low].reconstruction_threshold, low_threshold); assert_eq!(keyshares[&high].reconstruction_threshold, high_threshold); Ok(()) }, ) .await .unwrap(); }) .await; } }