diff --git a/Cargo.lock b/Cargo.lock index 45be58c34..5134118e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14096,9 +14096,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -14119,9 +14119,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", diff --git a/crates/client/entropy_metadata.scale b/crates/client/entropy_metadata.scale index a9c157862..9748b94ce 100644 Binary files a/crates/client/entropy_metadata.scale and b/crates/client/entropy_metadata.scale differ diff --git a/crates/protocol/src/execute_protocol.rs b/crates/protocol/src/execute_protocol.rs index 3e46b02db..9d95b6b20 100644 --- a/crates/protocol/src/execute_protocol.rs +++ b/crates/protocol/src/execute_protocol.rs @@ -69,7 +69,7 @@ impl RandomizedPrehashSigner for PairWrapper { } } -async fn execute_protocol_generic( +pub async fn execute_protocol_generic( mut chans: Channels, session: Session, session_id_hash: [u8; 32], diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 79443ea59..2b1b77ab7 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -160,7 +160,7 @@ pub enum SessionId { /// A distributed key generation protocol session for registering Dkg { user: AccountId32, block_number: u32 }, /// A proactive refresh session - ProactiveRefresh { verifying_key: Vec, block_number: u32 }, + Reshare { verifying_key: Vec, block_number: u32 }, /// A signing session Sign(SigningSessionInfo), } @@ -185,7 +185,7 @@ impl Hash for SessionId { user.0.hash(state); block_number.hash(state); }, - SessionId::ProactiveRefresh { verifying_key, block_number } => { + SessionId::Reshare { verifying_key, block_number } => { verifying_key.hash(state); block_number.hash(state); }, diff --git a/crates/protocol/tests/helpers/mod.rs b/crates/protocol/tests/helpers/mod.rs index 972bac0f2..627d1c912 100644 --- a/crates/protocol/tests/helpers/mod.rs +++ b/crates/protocol/tests/helpers/mod.rs @@ -55,7 +55,7 @@ struct ServerState { #[derive(Clone)] pub enum ProtocolOutput { Sign(RecoverableSignature), - ProactiveRefresh(ThresholdKeyShare), + Reshare(ThresholdKeyShare), Dkg(KeyShareWithAuxInfo), } @@ -130,7 +130,7 @@ pub async fn server( let (signature, recovery_id) = rsig.to_backend(); Ok(ProtocolOutput::Sign(RecoverableSignature { signature, recovery_id })) }, - SessionId::ProactiveRefresh { .. } => { + SessionId::Reshare { .. } => { let new_keyshare = execute_proactive_refresh( session_id, channels, @@ -139,7 +139,7 @@ pub async fn server( threshold_keyshare.unwrap(), ) .await?; - Ok(ProtocolOutput::ProactiveRefresh(new_keyshare)) + Ok(ProtocolOutput::Reshare(new_keyshare)) }, SessionId::Dkg { .. } => { let keyshare_and_aux_info = diff --git a/crates/protocol/tests/protocol.rs b/crates/protocol/tests/protocol.rs index 79aef51ee..a2e287203 100644 --- a/crates/protocol/tests/protocol.rs +++ b/crates/protocol/tests/protocol.rs @@ -113,7 +113,7 @@ async fn test_refresh_with_parties(num_parties: usize) { let keyshares = KeyShare::::new_centralized(&mut OsRng, &ids, None); let verifying_key = keyshares[&PartyId::from(pairs[0].public())].verifying_key(); - let session_id = SessionId::ProactiveRefresh { + let session_id = SessionId::Reshare { verifying_key: verifying_key.to_encoded_point(true).as_bytes().to_vec(), block_number: 0, }; @@ -131,7 +131,7 @@ async fn test_refresh_with_parties(num_parties: usize) { .collect(); let threshold = parties.len(); let mut outputs = test_protocol_with_parties(parties, session_id, threshold).await; - if let ProtocolOutput::ProactiveRefresh(keyshare) = outputs.pop().unwrap() { + if let ProtocolOutput::Reshare(keyshare) = outputs.pop().unwrap() { assert!(keyshare.verifying_key() == verifying_key); } else { panic!("Unexpected protocol output"); diff --git a/crates/shared/src/types.rs b/crates/shared/src/types.rs index ecec165e1..9f860d031 100644 --- a/crates/shared/src/types.rs +++ b/crates/shared/src/types.rs @@ -50,6 +50,16 @@ pub struct OcwMessageDkg { pub validators_info: Vec, } +/// Offchain worker message for initiating a refresh +#[cfg(not(feature = "wasm"))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo)] +pub struct OcwMessageReshare { + // Stash address of new signer + pub new_signer: Vec, + pub block_number: BlockNumber, +} + /// Offchain worker message for initiating a proactive refresh #[cfg(not(feature = "wasm"))] #[derive( diff --git a/crates/threshold-signature-server/src/helpers/substrate.rs b/crates/threshold-signature-server/src/helpers/substrate.rs index 9cb99b308..5b036275c 100644 --- a/crates/threshold-signature-server/src/helpers/substrate.rs +++ b/crates/threshold-signature-server/src/helpers/substrate.rs @@ -28,6 +28,7 @@ use crate::{ user::UserErr, }; pub use entropy_client::substrate::{query_chain, submit_transaction}; +use entropy_shared::user::ValidatorInfo; use subxt::{backend::legacy::LegacyRpcMethods, utils::AccountId32, Config, OnlineClient}; /// Given a threshold server's account ID, return its corresponding stash (validator) address. @@ -72,3 +73,41 @@ pub async fn get_registered_details( .ok_or_else(|| UserErr::ChainFetch("Not Registering error: Register Onchain first"))?; Ok(result) } + +/// Takes Stash keys and returns validator info from chain +pub async fn get_validators_info( + api: &OnlineClient, + rpc: &LegacyRpcMethods, + validators: Vec, +) -> Result, UserErr> { + let mut handles = Vec::new(); + let block_hash = rpc.chain_get_block_hash(None).await?; + for validator in validators { + let handle: tokio::task::JoinHandle> = tokio::task::spawn({ + let api = api.clone(); + let rpc = rpc.clone(); + + async move { + let threshold_address_query = + entropy::storage().staking_extension().threshold_servers(validator); + let server_info = query_chain(&api, &rpc, threshold_address_query, block_hash) + .await? + .ok_or_else(|| { + UserErr::OptionUnwrapError("Failed to unwrap validator info".to_string()) + })?; + + Ok(ValidatorInfo { + x25519_public_key: server_info.x25519_public_key, + ip_address: std::str::from_utf8(&server_info.endpoint)?.to_string(), + tss_account: server_info.tss_account, + }) + } + }); + handles.push(handle); + } + let mut all_signers: Vec = vec![]; + for handle in handles { + all_signers.push(handle.await.unwrap().unwrap()); + } + Ok(all_signers) +} diff --git a/crates/threshold-signature-server/src/helpers/tests.rs b/crates/threshold-signature-server/src/helpers/tests.rs index 8f7a9e3be..79226ee35 100644 --- a/crates/threshold-signature-server/src/helpers/tests.rs +++ b/crates/threshold-signature-server/src/helpers/tests.rs @@ -39,7 +39,7 @@ use crate::{ use axum::{routing::IntoMakeService, Router}; use entropy_kvdb::{encrypted_sled::PasswordMethod, get_db_path, kv_manager::KvManager}; use entropy_protocol::PartyId; -use entropy_shared::{DAVE_VERIFYING_KEY, EVE_VERIFYING_KEY}; +use entropy_shared::{DAVE_VERIFYING_KEY, EVE_VERIFYING_KEY, NETWORK_PARENT_KEY}; use std::time::Duration; use subxt::{ backend::legacy::LegacyRpcMethods, ext::sp_core::sr25519, tx::PairSigner, @@ -118,7 +118,7 @@ pub async fn create_clients( } /// Spawn 3 TSS nodes with pre-stored keyshares -pub async fn spawn_testing_validators() -> (Vec, Vec) { +pub async fn spawn_testing_validators(add_parent_key: bool) -> (Vec, Vec) { // spawn threshold servers let ports = [3001i64, 3002, 3003]; @@ -143,9 +143,9 @@ pub async fn spawn_testing_validators() -> (Vec, Vec) { let ids = vec![alice_id, bob_id, charlie_id]; - put_keyshares_in_db("alice", alice_kv).await; - put_keyshares_in_db("bob", bob_kv).await; - put_keyshares_in_db("charlie", charlie_kv).await; + put_keyshares_in_db("alice", alice_kv, add_parent_key).await; + put_keyshares_in_db("bob", bob_kv, add_parent_key).await; + put_keyshares_in_db("charlie", charlie_kv, add_parent_key).await; let listener_alice = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", ports[0])) .await @@ -175,9 +175,12 @@ pub async fn spawn_testing_validators() -> (Vec, Vec) { } /// Add the pre-generated test keyshares to a kvdb -async fn put_keyshares_in_db(holder_name: &str, kvdb: KvManager) { - let user_names_and_verifying_keys = [("eve", EVE_VERIFYING_KEY), ("dave", DAVE_VERIFYING_KEY)]; - +async fn put_keyshares_in_db(holder_name: &str, kvdb: KvManager, add_parent_key: bool) { + let mut user_names_and_verifying_keys = + vec![("eve", hex::encode(EVE_VERIFYING_KEY)), ("dave", hex::encode(DAVE_VERIFYING_KEY))]; + if add_parent_key { + user_names_and_verifying_keys.push(("eve", hex::encode(NETWORK_PARENT_KEY))) + } for (user_name, user_verifying_key) in user_names_and_verifying_keys { let keyshare_bytes = { let project_root = @@ -188,7 +191,7 @@ async fn put_keyshares_in_db(holder_name: &str, kvdb: KvManager) { )); std::fs::read(file_path).unwrap() }; - let reservation = kvdb.kv().reserve_key(hex::encode(user_verifying_key)).await.unwrap(); + let reservation = kvdb.kv().reserve_key(user_verifying_key).await.unwrap(); kvdb.kv().put(reservation, keyshare_bytes).await.unwrap(); } } diff --git a/crates/threshold-signature-server/src/helpers/validator.rs b/crates/threshold-signature-server/src/helpers/validator.rs index 6b80ad2b0..b571e0181 100644 --- a/crates/threshold-signature-server/src/helpers/validator.rs +++ b/crates/threshold-signature-server/src/helpers/validator.rs @@ -68,7 +68,7 @@ fn get_hkdf_from_mnemonic(mnemonic: &str) -> Result, UserErr> { } /// Derive signing keypair -fn get_signer_from_hkdf( +pub fn get_signer_from_hkdf( hkdf: &Hkdf, ) -> Result, UserErr> { let mut sr25519_seed = [0u8; 32]; diff --git a/crates/threshold-signature-server/src/lib.rs b/crates/threshold-signature-server/src/lib.rs index 0c06f9693..67dfb086c 100644 --- a/crates/threshold-signature-server/src/lib.rs +++ b/crates/threshold-signature-server/src/lib.rs @@ -86,10 +86,10 @@ //! in a [crate::validation::SignedMessage]. //! //! - [`/ws`](crate::signing_client::api::ws_handler()) - Websocket server for signing and DKG protocol -//! messages. This is opened by other threshold servers when the signing procotol is initiated. +//! messages. This is opened by other threshold servers when the signing procotol is initiated. //! //! - [`/validator/sync_kvdb`](crate::validator::api::sync_kvdb()) - POST - Called by another -//! threshold server when joining to get the key-shares from a member of their sub-group. +//! threshold server when joining to get the key-shares from a member of their sub-group. //! //! Takes a list of users account IDs for which shares are requested, wrapped in a //! [crate::validation::SignedMessage]. @@ -119,7 +119,7 @@ //! //! - Axum server - Includes global state and mutex locked IPs //! - [kvdb](entropy_kvdb) - Encrypted key-value database for storing key-shares and other data, build using -//! [sled](https://docs.rs/sled) +//! [sled](https://docs.rs/sled) #![doc(html_logo_url = "https://entropy.xyz/assets/logo_02.png")] pub use entropy_client::chain_api; pub(crate) mod health; @@ -155,6 +155,7 @@ use crate::{ r#unsafe::api::{delete, put, remove_keys, unsafe_get}, signing_client::{api::*, ListenerState}, user::api::*, + validator::api::new_reshare, }; #[derive(Clone)] @@ -176,6 +177,7 @@ pub fn app(app_state: AppState) -> Router { .route("/user/new", post(new_user)) .route("/user/sign_tx", post(sign_tx)) .route("/signer/proactive_refresh", post(proactive_refresh)) + .route("/validator/reshare", post(new_reshare)) .route("/healthz", get(healthz)) .route("/version", get(get_version)) .route("/hashes", get(hashes)) diff --git a/crates/threshold-signature-server/src/signing_client/api.rs b/crates/threshold-signature-server/src/signing_client/api.rs index c30ba7f64..8c3858ef6 100644 --- a/crates/threshold-signature-server/src/signing_client/api.rs +++ b/crates/threshold-signature-server/src/signing_client/api.rs @@ -157,7 +157,7 @@ pub async fn do_proactive_refresh( tracing::debug!("Preparing to perform proactive refresh"); tracing::debug!("Signing with {:?}", &signer.signer().public()); - let session_id = SessionId::ProactiveRefresh { verifying_key, block_number }; + let session_id = SessionId::Reshare { verifying_key, block_number }; let account_id = SubxtAccountId32(signer.signer().public().0); let mut converted_validator_info = vec![]; let mut tss_accounts = vec![]; diff --git a/crates/threshold-signature-server/src/signing_client/tests.rs b/crates/threshold-signature-server/src/signing_client/tests.rs index 2bdba0861..b4a9ee4ff 100644 --- a/crates/threshold-signature-server/src/signing_client/tests.rs +++ b/crates/threshold-signature-server/src/signing_client/tests.rs @@ -45,7 +45,7 @@ async fn test_proactive_refresh() { clean_tests(); let _cxt = test_node_process_testing_state(false).await; - let (validator_ips, _ids) = spawn_testing_validators().await; + let (validator_ips, _ids) = spawn_testing_validators(false).await; let client = reqwest::Client::new(); diff --git a/crates/threshold-signature-server/src/user/tests.rs b/crates/threshold-signature-server/src/user/tests.rs index 908237e4a..ab0506e42 100644 --- a/crates/threshold-signature-server/src/user/tests.rs +++ b/crates/threshold-signature-server/src/user/tests.rs @@ -167,7 +167,7 @@ async fn test_sign_tx_no_chain() { let one = AccountKeyring::Dave; let two = AccountKeyring::Two; - let (_validator_ips, _validator_ids) = spawn_testing_validators().await; + let (_validator_ips, _validator_ids) = spawn_testing_validators(false).await; let substrate_context = test_context_stationary().await; let entropy_api = get_api(&substrate_context.node_proc.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.node_proc.ws_url).await.unwrap(); @@ -369,7 +369,7 @@ async fn test_sign_tx_no_chain_fail() { let one = AccountKeyring::Dave; - let (_validator_ips, _validator_ids) = spawn_testing_validators().await; + let (_validator_ips, _validator_ids) = spawn_testing_validators(false).await; let substrate_context = test_context_stationary().await; let entropy_api = get_api(&substrate_context.node_proc.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.node_proc.ws_url).await.unwrap(); @@ -493,7 +493,7 @@ async fn test_program_with_config() { let one = AccountKeyring::Dave; let two = AccountKeyring::Two; - let (_validator_ips, _validator_ids) = spawn_testing_validators().await; + let (_validator_ips, _validator_ids) = spawn_testing_validators(false).await; let substrate_context = test_context_stationary().await; let entropy_api = get_api(&substrate_context.node_proc.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.node_proc.ws_url).await.unwrap(); @@ -559,7 +559,7 @@ async fn test_store_share() { let program_manager = AccountKeyring::Dave; let cxt = test_context_stationary().await; - let (_validator_ips, _validator_ids) = spawn_testing_validators().await; + let (_validator_ips, _validator_ids) = spawn_testing_validators(false).await; let api = get_api(&cxt.node_proc.ws_url).await.unwrap(); let rpc = get_rpc(&cxt.node_proc.ws_url).await.unwrap(); @@ -730,7 +730,7 @@ async fn test_jumpstart_network() { let alice = AccountKeyring::Alice; let cxt = test_context_stationary().await; - let (_validator_ips, _validator_ids) = spawn_testing_validators().await; + let (_validator_ips, _validator_ids) = spawn_testing_validators(false).await; let api = get_api(&cxt.node_proc.ws_url).await.unwrap(); let rpc = get_rpc(&cxt.node_proc.ws_url).await.unwrap(); @@ -927,7 +927,7 @@ async fn test_fail_infinite_program() { let one = AccountKeyring::Dave; let two = AccountKeyring::Two; - let (validator_ips, _validator_ids) = spawn_testing_validators().await; + let (validator_ips, _validator_ids) = spawn_testing_validators(false).await; let substrate_context = test_context_stationary().await; let entropy_api = get_api(&substrate_context.node_proc.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.node_proc.ws_url).await.unwrap(); @@ -1026,7 +1026,7 @@ async fn test_device_key_proxy() { let one = AccountKeyring::Dave; - let (_validator_ips, _validator_ids) = spawn_testing_validators().await; + let (_validator_ips, _validator_ids) = spawn_testing_validators(false).await; let substrate_context = test_context_stationary().await; let entropy_api = get_api(&substrate_context.node_proc.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.node_proc.ws_url).await.unwrap(); @@ -1134,7 +1134,7 @@ async fn test_faucet() { let two = AccountKeyring::Eve; let alice = AccountKeyring::Alice; - let (validator_ips, _validator_ids) = spawn_testing_validators().await; + let (validator_ips, _validator_ids) = spawn_testing_validators(false).await; let substrate_context = test_node_process_testing_state(true).await; let entropy_api = get_api(&substrate_context.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.ws_url).await.unwrap(); diff --git a/crates/threshold-signature-server/src/validator/api.rs b/crates/threshold-signature-server/src/validator/api.rs index b8234e8a0..e7e6738b5 100644 --- a/crates/threshold-signature-server/src/validator/api.rs +++ b/crates/threshold-signature-server/src/validator/api.rs @@ -16,13 +16,195 @@ use crate::{ chain_api::{ entropy::{self}, - EntropyConfig, + get_api, get_rpc, EntropyConfig, }, - helpers::{launch::FORBIDDEN_KEYS, substrate::query_chain}, + get_signer_and_x25519_secret, + helpers::{ + launch::FORBIDDEN_KEYS, + substrate::{get_stash_address, get_validators_info, query_chain}, + }, + signing_client::{protocol_transport::open_protocol_connections, ProtocolErr}, validator::errors::ValidatorErr, + AppState, +}; +use axum::{body::Bytes, extract::State, http::StatusCode}; +use entropy_kvdb::kv_manager::helpers::serialize as key_serialize; +pub use entropy_protocol::{ + decode_verifying_key, + errors::ProtocolExecutionErr, + execute_protocol::{execute_protocol_generic, Channels, PairWrapper}, + KeyParams, KeyShareWithAuxInfo, Listener, PartyId, SessionId, ValidatorInfo, }; -use std::str::FromStr; +use entropy_shared::{OcwMessageReshare, NETWORK_PARENT_KEY, SETUP_TIMEOUT_SECONDS}; +use parity_scale_codec::{Decode, Encode}; +use rand_core::OsRng; +use sp_core::Pair; +use std::{collections::BTreeSet, str::FromStr, time::Duration}; use subxt::{backend::legacy::LegacyRpcMethods, utils::AccountId32, OnlineClient}; +use synedrion::{ + make_key_resharing_session, sessions::SessionId as SynedrionSessionId, KeyResharingInputs, + NewHolder, OldHolder, +}; +use tokio::time::timeout; + +/// HTTP POST endpoint called by the off-chain worker (propagation pallet) during network reshare. +/// +/// The HTTP request takes a Parity SCALE encoded [OcwMessageReshare] which indicates which validator is joining +/// +/// This will trigger the key reshare process. +#[tracing::instrument(skip_all)] +pub async fn new_reshare( + State(app_state): State, + encoded_data: Bytes, +) -> Result { + let data = OcwMessageReshare::decode(&mut encoded_data.as_ref())?; + // TODO: validate message came from chain (check reshare block # against current block number) see #941 + + let api = get_api(&app_state.configuration.endpoint).await?; + let rpc = get_rpc(&app_state.configuration.endpoint).await?; + + let signers_query = entropy::storage().staking_extension().signers(); + let signers = query_chain(&api, &rpc, signers_query, None) + .await? + .ok_or_else(|| ValidatorErr::ChainFetch("Error getting signers"))?; + + let next_signers_query = entropy::storage().staking_extension().signers(); + let next_signers = query_chain(&api, &rpc, next_signers_query, None) + .await? + .ok_or_else(|| ValidatorErr::ChainFetch("Error getting next signers"))?; + + let validators_info = get_validators_info(&api, &rpc, next_signers) + .await + .map_err(|e| ValidatorErr::UserError(e.to_string()))?; + let (signer, x25519_secret_key) = get_signer_and_x25519_secret(&app_state.kv_store) + .await + .map_err(|e| ValidatorErr::UserError(e.to_string()))?; + + let verifying_key_query = entropy::storage().registry().jump_start_progress(); + let verifying_key = query_chain(&api, &rpc, verifying_key_query, None) + .await? + .ok_or_else(|| ValidatorErr::ChainFetch("Parent verifying key error"))? + .verifying_key + .ok_or_else(|| ValidatorErr::OptionUnwrapError("Failed to get verifying key".to_string()))? + .0; + + let decoded_verifying_key = decode_verifying_key( + &verifying_key + .clone() + .try_into() + .map_err(|_| ValidatorErr::Conversion("Verifying key conversion"))?, + ) + .map_err(|e| ValidatorErr::VerifyingKeyError(e.to_string()))?; + + let is_proper_signer = &validators_info + .iter() + .any(|validator_info| validator_info.tss_account == *signer.account_id()); + + if !is_proper_signer { + return Ok(StatusCode::MISDIRECTED_REQUEST); + } + // get old key if have it + let my_stash_address = get_stash_address(&api, &rpc, signer.account_id()) + .await + .map_err(|e| ValidatorErr::UserError(e.to_string()))?; + let old_holder: Option> = + if data.new_signer == my_stash_address.encode() { + None + } else { + let kvdb_result = app_state.kv_store.kv().get(&hex::encode(NETWORK_PARENT_KEY)).await?; + let key_share: KeyShareWithAuxInfo = + entropy_kvdb::kv_manager::helpers::deserialize(&kvdb_result) + .ok_or_else(|| ValidatorErr::KvDeserialize("Failed to load KeyShare".into()))?; + Some(OldHolder { key_share: key_share.0 }) + }; + let party_ids: BTreeSet = + validators_info.iter().cloned().map(|x| PartyId::new(x.tss_account)).collect(); + + let old_holders_info = get_validators_info(&api, &rpc, signers) + .await + .map_err(|e| ValidatorErr::UserError(e.to_string()))?; + let old_holders: BTreeSet = + old_holders_info.iter().cloned().map(|x| PartyId::new(x.tss_account)).collect(); + + let new_holder = NewHolder { + verifying_key: decoded_verifying_key, + // TODO: get from chain see #941 + old_threshold: party_ids.len(), + old_holders, + }; + let key_info_query = entropy::storage().parameters().signers_info(); + let threshold = query_chain(&api, &rpc, key_info_query, None) + .await? + .ok_or_else(|| ValidatorErr::ChainFetch("Failed to get signers info"))? + .threshold; + + let inputs = KeyResharingInputs { + old_holder, + new_holder: Some(new_holder), + new_holders: party_ids.clone(), + new_threshold: threshold as usize, + }; + + let session_id = SessionId::Reshare { verifying_key, block_number: data.block_number }; + let account_id = AccountId32(signer.signer().public().0); + let session_id_hash = session_id.blake2(None)?; + let pair = PairWrapper(signer.signer().clone()); + + let mut converted_validator_info = vec![]; + let mut tss_accounts = vec![]; + for validator_info in validators_info { + let validator_info = ValidatorInfo { + x25519_public_key: validator_info.x25519_public_key, + ip_address: validator_info.ip_address, + tss_account: validator_info.tss_account.clone(), + }; + converted_validator_info.push(validator_info.clone()); + tss_accounts.push(validator_info.tss_account.clone()); + } + + let (rx_ready, rx_from_others, listener) = + Listener::new(converted_validator_info.clone(), &account_id); + app_state + .listener_state + .listeners + .lock() + .map_err(|_| ValidatorErr::SessionError("Error getting lock".to_string()))? + .insert(session_id.clone(), listener); + + open_protocol_connections( + &converted_validator_info, + &session_id, + signer.signer(), + &app_state.listener_state, + &x25519_secret_key, + ) + .await?; + + let channels = { + let ready = timeout(Duration::from_secs(SETUP_TIMEOUT_SECONDS), rx_ready).await?; + let broadcast_out = ready??; + Channels(broadcast_out, rx_from_others) + }; + + let session = make_key_resharing_session( + &mut OsRng, + SynedrionSessionId::from_seed(session_id_hash.as_slice()), + pair, + &party_ids, + inputs, + ) + .map_err(ProtocolExecutionErr::SessionCreation)?; + + let new_key_share = execute_protocol_generic(channels, session, session_id_hash) + .await + .map_err(|_| ValidatorErr::ProtocolError("Error executing protocol".to_string()))? + .0 + .ok_or(ValidatorErr::NoOutputFromReshareProtocol)?; + let _serialized_key_share = key_serialize(&new_key_share) + .map_err(|_| ProtocolErr::KvSerialize("Kv Serialize Error".to_string()))?; + // TODO: do reshare call confirm_reshare (delete key when done) see #941 + Ok(StatusCode::OK) +} /// Validation for if an account can cover tx fees for a tx pub async fn check_balance_for_fees( diff --git a/crates/threshold-signature-server/src/validator/errors.rs b/crates/threshold-signature-server/src/validator/errors.rs index bb1b075a8..6b8c7dae6 100644 --- a/crates/threshold-signature-server/src/validator/errors.rs +++ b/crates/threshold-signature-server/src/validator/errors.rs @@ -15,12 +15,15 @@ use std::string::FromUtf8Error; +use crate::signing_client::ProtocolErr; use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; -use entropy_protocol::sign_and_encrypt::EncryptedSignedMessageErr; +use entropy_protocol::{errors::ProtocolExecutionErr, sign_and_encrypt::EncryptedSignedMessageErr}; +use synedrion::sessions; use thiserror::Error; +use tokio::sync::oneshot::error::RecvError; #[derive(Debug, Error)] pub enum ValidatorErr { @@ -52,6 +55,38 @@ pub enum ValidatorErr { Authentication, #[error("Substrate: {0}")] SubstrateClient(#[from] entropy_client::substrate::SubstrateError), + #[error("Codec decoding error: {0}")] + CodecError(#[from] parity_scale_codec::Error), + #[error("User Error: {0}")] + UserError(String), + #[error("Option Unwrap error: {0}")] + OptionUnwrapError(String), + #[error("Vec Conversion Error: {0}")] + Conversion(&'static str), + #[error("Verifying key Error: {0}")] + VerifyingKeyError(String), + #[error("Session Error: {0}")] + SessionError(String), + #[error("Protocol Execution Error {0}")] + ProtocolExecution(#[from] ProtocolExecutionErr), + #[error("Listener: {0}")] + Listener(#[from] entropy_protocol::errors::ListenerErr), + #[error("Reshare protocol error: {0}")] + SigningClientError(#[from] ProtocolErr), + #[error("Timed out waiting for remote party")] + Timeout(#[from] tokio::time::error::Elapsed), + #[error("Oneshot timeout error: {0}")] + OneshotTimeout(#[from] RecvError), + #[error("Synedrion session creation error: {0}")] + SessionCreation(sessions::LocalError), + #[error("No output from reshare protocol")] + NoOutputFromReshareProtocol, + #[error("Protocol Error: {0}")] + ProtocolError(String), + #[error("Kv Fatal error")] + KvSerialize(String), + #[error("Kv Deserialization Error: {0}")] + KvDeserialize(String), } impl IntoResponse for ValidatorErr { diff --git a/crates/threshold-signature-server/src/validator/tests.rs b/crates/threshold-signature-server/src/validator/tests.rs index 6f2090642..cc4bb3efd 100644 --- a/crates/threshold-signature-server/src/validator/tests.rs +++ b/crates/threshold-signature-server/src/validator/tests.rs @@ -12,19 +12,96 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use entropy_shared::MIN_BALANCE; +use super::api::{check_balance_for_fees, check_forbidden_key}; +use crate::{ + chain_api::{ + entropy::{self, runtime_types::bounded_collections::bounded_vec}, + get_api, get_rpc, EntropyConfig, + }, + helpers::{ + launch::{development_mnemonic, ValidatorName, FORBIDDEN_KEYS}, + substrate::submit_transaction, + tests::initialize_test_logger, + validator::get_signer_and_x25519_secret_from_mnemonic, + }, + validator::errors::ValidatorErr, +}; +use entropy_kvdb::clean_tests; +use entropy_shared::{OcwMessageReshare, EVE_VERIFYING_KEY, MIN_BALANCE}; use entropy_testing_utils::{ constants::{ALICE_STASH_ADDRESS, RANDOM_ACCOUNT}, + spawn_testing_validators, substrate_context::testing_context, + test_context_stationary, }; - -use super::api::{check_balance_for_fees, check_forbidden_key}; -use crate::{ - chain_api::{get_api, get_rpc}, - helpers::{launch::FORBIDDEN_KEYS, tests::initialize_test_logger}, - validator::errors::ValidatorErr, +use futures::future::join_all; +use parity_scale_codec::Encode; +use serial_test::serial; +use sp_keyring::AccountKeyring; +use subxt::{ + backend::legacy::LegacyRpcMethods, ext::sp_core::sr25519, tx::PairSigner, OnlineClient, }; +#[tokio::test] +#[serial] +async fn test_reshare() { + initialize_test_logger().await; + clean_tests(); + + let alice = AccountKeyring::Alice; + + let cxt = test_context_stationary().await; + let (_validator_ips, _validator_ids) = spawn_testing_validators(true).await; + let api = get_api(&cxt.node_proc.ws_url).await.unwrap(); + let rpc = get_rpc(&cxt.node_proc.ws_url).await.unwrap(); + + let client = reqwest::Client::new(); + let block_number = rpc.chain_get_header(None).await.unwrap().unwrap().number + 1; + + let onchain_reshare_request = + OcwMessageReshare { new_signer: alice.public().encode(), block_number }; + setup_for_reshare(&api, &rpc).await; + + let response_results = join_all( + vec![3001, 3002, 3003] + .iter() + .map(|port| { + client + .post(format!("http://127.0.0.1:{}/validator/reshare", port)) + .body(onchain_reshare_request.clone().encode()) + .send() + }) + .collect::>(), + ) + .await; + for response_result in response_results { + assert_eq!(response_result.unwrap().text().await.unwrap(), ""); + } + clean_tests(); +} + +async fn setup_for_reshare( + api: &OnlineClient, + rpc: &LegacyRpcMethods, +) { + let alice = AccountKeyring::Alice; + let signer = PairSigner::::new(alice.clone().into()); + + let jump_start_request = entropy::tx().registry().jump_start_network(); + let _result = submit_transaction(api, rpc, &signer, &jump_start_request, None).await.unwrap(); + + let validators_names = vec![ValidatorName::Alice, ValidatorName::Bob]; + for validator_name in validators_names { + let mnemonic = development_mnemonic(&Some(validator_name)); + let (tss_signer, _static_secret) = + get_signer_and_x25519_secret_from_mnemonic(&mnemonic.to_string()).unwrap(); + let jump_start_confirm_request = entropy::tx() + .registry() + .confirm_jump_start(bounded_vec::BoundedVec(EVE_VERIFYING_KEY.to_vec())); + + submit_transaction(api, rpc, &tss_signer, &jump_start_confirm_request, None).await.unwrap(); + } +} #[tokio::test] #[should_panic = "Account does not exist, add balance"] async fn test_check_balance_for_fees() { diff --git a/crates/threshold-signature-server/tests/sign.rs b/crates/threshold-signature-server/tests/sign.rs index 8acc5295d..9ad3cd48b 100644 --- a/crates/threshold-signature-server/tests/sign.rs +++ b/crates/threshold-signature-server/tests/sign.rs @@ -40,7 +40,7 @@ async fn integration_test_sign_public() { let eve = AccountKeyring::Eve; let request_author = AccountKeyring::One; - let (_validator_ips, _validator_ids) = spawn_testing_validators().await; + let (_validator_ips, _validator_ids) = spawn_testing_validators(false).await; let substrate_context = test_context_stationary().await; let api = get_api(&substrate_context.node_proc.ws_url).await.unwrap(); diff --git a/crates/threshold-signature-server/tests/sign_eth_tx.rs b/crates/threshold-signature-server/tests/sign_eth_tx.rs index 8991bb953..c957f5148 100644 --- a/crates/threshold-signature-server/tests/sign_eth_tx.rs +++ b/crates/threshold-signature-server/tests/sign_eth_tx.rs @@ -48,7 +48,7 @@ async fn integration_test_sign_eth_tx() { clean_tests(); let pre_registered_user = AccountKeyring::Eve; - let (_validator_ips, _validator_ids) = spawn_testing_validators().await; + let (_validator_ips, _validator_ids) = spawn_testing_validators(false).await; let substrate_context = test_context_stationary().await; let api = get_api(&substrate_context.node_proc.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.node_proc.ws_url).await.unwrap(); diff --git a/node/cli/src/chain_spec/testnet.rs b/node/cli/src/chain_spec/testnet.rs index 9824db2d4..c304a555d 100644 --- a/node/cli/src/chain_spec/testnet.rs +++ b/node/cli/src/chain_spec/testnet.rs @@ -202,7 +202,7 @@ pub fn testnet_local_initial_tss_servers() -> Vec<(TssAccountId, TssX25519Public /// However, this can be done by: /// - First, spinning up the machines you expect to be running at genesis /// - Then, running each TSS server with the `--setup-only` flag to get the `TssAccountId` and -/// `TssX25519PublicKey` +/// `TssX25519PublicKey` /// - Finally, writing all that information back here, and generating the chainspec from that. /// /// Note that if the KVDB of the TSS is deleted at any point during this process you will end up diff --git a/node/cli/src/service.rs b/node/cli/src/service.rs index a26bb59c0..bf5a28bbd 100644 --- a/node/cli/src/service.rs +++ b/node/cli/src/service.rs @@ -367,6 +367,11 @@ pub fn new_full_base( b"refresh", &format!("{}/signer/proactive_refresh", endpoint).into_bytes(), ); + offchain_db.local_storage_set( + sp_core::offchain::StorageKind::PERSISTENT, + b"reshare_validators", + &format!("{}/validator/reshare", endpoint).into_bytes(), + ); log::info!("Threshold Signing Sever (TSS) location changed to {}", endpoint); } } diff --git a/pallets/propagation/src/lib.rs b/pallets/propagation/src/lib.rs index 31260c546..99062ca39 100644 --- a/pallets/propagation/src/lib.rs +++ b/pallets/propagation/src/lib.rs @@ -30,7 +30,9 @@ mod tests; #[frame_support::pallet] pub mod pallet { use codec::Encode; - use entropy_shared::{OcwMessageDkg, OcwMessageProactiveRefresh, ValidatorInfo}; + use entropy_shared::{ + OcwMessageDkg, OcwMessageProactiveRefresh, OcwMessageReshare, ValidatorInfo, + }; use frame_support::{pallet_prelude::*, sp_runtime::traits::Saturating}; use frame_system::pallet_prelude::*; use sp_runtime::{ @@ -56,6 +58,7 @@ pub mod pallet { impl Hooks> for Pallet { fn offchain_worker(block_number: BlockNumberFor) { let _ = Self::post_dkg(block_number); + let _ = Self::post_reshare(block_number); let _ = Self::post_user_registration(block_number); let _ = Self::post_proactive_refresh(block_number); } @@ -77,6 +80,10 @@ pub mod pallet { /// Proactive Refresh Message passed to validators /// parameters. [OcwMessageProactiveRefresh] ProactiveRefreshMessagePassed(OcwMessageProactiveRefresh), + + /// Proactive Refresh Message passed to validators + /// parameters. [OcwMessageReshare] + KeyReshareMessagePassed(OcwMessageReshare), } #[pallet::call] @@ -201,6 +208,54 @@ pub mod pallet { Ok(()) } + /// Submits a request to do a key refresh on the signers parent key. + pub fn post_reshare(block_number: BlockNumberFor) -> Result<(), http::Error> { + let reshare_data = pallet_staking_extension::Pallet::::reshare_data(); + if reshare_data.block_number != block_number { + return Ok(()); + } + + let deadline = sp_io::offchain::timestamp().add(Duration::from_millis(2_000)); + let kind = sp_core::offchain::StorageKind::PERSISTENT; + let from_local = sp_io::offchain::local_storage_get(kind, b"reshare_validators") + .unwrap_or_else(|| b"http://localhost:3001/validator/reshare".to_vec()); + let url = + str::from_utf8(&from_local).unwrap_or("http://localhost:3001/validator/reshare"); + let converted_block_number: u32 = + BlockNumberFor::::try_into(block_number).unwrap_or_default(); + + let req_body = OcwMessageReshare { + new_signer: reshare_data.new_signer, + // subtract 1 from blocknumber since the request is from the last block + block_number: converted_block_number.saturating_sub(1), + }; + + log::warn!("propagation::post::req_body reshare: {:?}", &[req_body.encode()]); + + // We construct the request + // important: the header->Content-Type must be added and match that of the receiving + // party!! + let pending = http::Request::post(url, vec![req_body.encode()]) + .deadline(deadline) + .send() + .map_err(|_| http::Error::IoError)?; + + // We await response, same as in fn get() + let response = + pending.try_wait(deadline).map_err(|_| http::Error::DeadlineReached)??; + + // check response code + if response.code != 200 { + log::warn!("Unexpected status code: {}", response.code); + return Err(http::Error::Unknown); + } + let _res_body = response.body().collect::>(); + + Self::deposit_event(Event::KeyReshareMessagePassed(req_body)); + + Ok(()) + } + /// Submits a request to perform a proactive refresh to the threshold servers. pub fn post_proactive_refresh(block_number: BlockNumberFor) -> Result<(), http::Error> { let refresh_info = pallet_staking_extension::Pallet::::proactive_refresh(); diff --git a/pallets/propagation/src/tests.rs b/pallets/propagation/src/tests.rs index 0f0bffa5f..b3e411e11 100644 --- a/pallets/propagation/src/tests.rs +++ b/pallets/propagation/src/tests.rs @@ -20,7 +20,7 @@ use entropy_shared::ValidatorInfo; use frame_support::{assert_ok, traits::OnInitialize, BoundedVec}; use pallet_programs::ProgramInfo; use pallet_registry::ProgramInstance; -use pallet_staking_extension::RefreshInfo; +use pallet_staking_extension::{RefreshInfo, ReshareInfo}; use sp_core::offchain::{testing, OffchainDbExt, OffchainWorkerExt, TransactionPoolExt}; use sp_io::TestExternalities; use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; @@ -73,6 +73,14 @@ fn knows_how_to_mock_several_http_calls() { .to_vec(), ..Default::default() }); + state.expect_request(testing::PendingRequest { + method: "POST".into(), + uri: "http://localhost:3001/validator/reshare".into(), + sent: true, + response: Some([].to_vec()), + body: [32, 1, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0].to_vec(), + ..Default::default() + }); }); t.execute_with(|| { @@ -120,6 +128,15 @@ fn knows_how_to_mock_several_http_calls() { Propagation::post_proactive_refresh(6).unwrap(); Propagation::on_initialize(6); assert_eq!(Staking::proactive_refresh(), RefreshInfo::default()); + + // doesn't trigger no reshare block + Propagation::post_reshare(7).unwrap(); + pallet_staking_extension::ReshareData::::put(ReshareInfo { + block_number: 7, + new_signer: 1u64.encode(), + }); + // now triggers + Propagation::post_reshare(7).unwrap(); }) } diff --git a/pallets/staking/src/lib.rs b/pallets/staking/src/lib.rs index 85f3073a9..8496a3b31 100644 --- a/pallets/staking/src/lib.rs +++ b/pallets/staking/src/lib.rs @@ -114,6 +114,11 @@ pub mod pallet { pub proactive_refresh_keys: Vec>, } + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, Default)] + pub struct ReshareInfo { + pub new_signer: Vec, + pub block_number: BlockNumber, + } #[pallet::pallet] #[pallet::without_storage_info] pub struct Pallet(_); @@ -170,6 +175,11 @@ pub mod pallet { #[pallet::getter(fn next_signers)] pub type NextSigners = StorageValue<_, Vec, ValueQuery>; + /// The next time a reshare should happen + #[pallet::storage] + #[pallet::getter(fn reshare_data)] + pub type ReshareData = StorageValue<_, ReshareInfo>, ValueQuery>; + /// A type used to simplify the genesis configuration definition. pub type ThresholdServersConfig = ( ::ValidatorId, @@ -437,9 +447,15 @@ pub mod pallet { current_signers.remove(0); current_signers.push(next_signer_up.clone()); NextSigners::::put(current_signers); + // trigger reshare at next block + let current_block_number = >::block_number(); + let reshare_info = ReshareInfo { + block_number: current_block_number + sp_runtime::traits::One::one(), + new_signer: next_signer_up.encode(), + }; + ReshareData::::put(reshare_info); // for next PR - // tell signers to do new key rotation with new signer group (dkg) // confirm action has taken place Ok(()) diff --git a/pallets/staking/src/tests.rs b/pallets/staking/src/tests.rs index b0142bdda..eb517982c 100644 --- a/pallets/staking/src/tests.rs +++ b/pallets/staking/src/tests.rs @@ -14,10 +14,10 @@ // along with this program. If not, see . use crate::{mock::*, tests::RuntimeEvent, Error, IsValidatorSynced, ServerInfo, ThresholdToStash}; +use codec::Encode; use frame_support::{assert_noop, assert_ok}; use frame_system::{EventRecord, Phase}; use pallet_session::SessionManager; - const NULL_ARR: [u8; 32] = [0; 32]; #[test] @@ -331,11 +331,24 @@ fn it_tests_new_session_handler() { // no next signers at start assert_eq!(Staking::next_signers().len(), 0); + assert_eq!(Staking::reshare_data().block_number, 0, "Check reshare block start at zero"); + System::set_block_number(100); assert_ok!(Staking::new_session_handler(&[1, 2, 3])); // takes signers original (5,6) pops off first 5, adds (fake randomness in mock so adds 1) assert_eq!(Staking::next_signers(), vec![6, 1]); + assert_eq!( + Staking::reshare_data().block_number, + 101, + "Check reshare block start at 100 + 1" + ); + assert_eq!( + Staking::reshare_data().new_signer, + 1u64.encode(), + "Check reshare next signer up is 1" + ); + assert_ok!(Staking::new_session_handler(&[6, 5, 3])); // takes 3 and leaves 5 and 6 since already in signer group assert_eq!(Staking::next_signers(), vec![6, 3]); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 53104c22a..ad26f4020 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1384,8 +1384,8 @@ impl pallet_transaction_storage::Config for Runtime { parameter_types! { pub const BagThresholds: &'static [u64] = &voter_bags::THRESHOLDS; } -type VoterBagsListInstance = pallet_bags_list::Instance1; -impl pallet_bags_list::Config for Runtime { + +impl pallet_bags_list::Config for Runtime { type BagThresholds = BagThresholds; type RuntimeEvent = RuntimeEvent; type Score = VoteWeight;