diff --git a/CHANGELOG.md b/CHANGELOG.md index 22baf545b..299523c18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,11 @@ At the moment this project **does not** adhere to pallet. The genesis build config was also removed. Additionally, the `new/user/` HTTP endpoint in the TSS was removed since it was no longer necessary. - In [#1045](https://github.com/entropyxyz/entropy-core/pull/1045), `ProgramsInfo` now takes `version_number` to maintain backwards compatibility if programs runtime is updated +- In [#1050](https://github.com/entropyxyz/entropy-core/pull/1050), the flow for signing has changed. + A user now sends their request to any validator that is not a signer. This will act as a relayer. + As such, `UserSignatureRequest` no longer requires the `validators_info` field since the the + relayer adds that in after. The response received from the validator is now a `Vec` + from the signers. ### Added - Jumpstart network ([#918](https://github.com/entropyxyz/entropy-core/pull/918)) @@ -40,6 +45,7 @@ At the moment this project **does not** adhere to ### Changed - Fix TSS `AccountId` keys in chainspec ([#993](https://github.com/entropyxyz/entropy-core/pull/993)) +- Add relay tx endpoint ([#1050](https://github.com/entropyxyz/entropy-core/pull/1050)) ### Removed - Remove `prune_registration` extrinsic ([#1022](https://github.com/entropyxyz/entropy-core/pull/1022)) diff --git a/Cargo.lock b/Cargo.lock index 47bbaeff3..b7e2d384a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2494,6 +2494,7 @@ dependencies = [ "hex", "js-sys", "num", + "rand", "rand_core 0.6.4", "reqwest", "serde", diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index b04c1b679..5da179d02 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -18,6 +18,7 @@ thiserror ="1.0.64" futures ="0.3" sp-core ={ version="31.0.0", default-features=false, features=["full_crypto", "serde"] } tracing ="0.1.37" +rand ={ version="0.8", default-features=false } # Present when "full-client" feature is active blake2 ={ version="0.10.4", optional=true } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index b5589c518..89c49514b 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -21,6 +21,7 @@ pub use crate::{ }; use anyhow::anyhow; pub use entropy_protocol::{sign_and_encrypt::EncryptedSignedMessage, KeyParams}; +use rand::Rng; use std::str::FromStr; pub use synedrion::KeyShare; @@ -38,17 +39,18 @@ use crate::{ }, client::entropy::staking_extension::events::{EndpointChanged, ThresholdAccountChanged}, substrate::{get_registered_details, submit_transaction_with_pair}, - user::{get_signers_from_chain, UserSignatureRequest}, + user::{get_all_signers_from_chain, get_validators_not_signer_for_relay, UserSignatureRequest}, Hasher, }; use base64::prelude::{Engine, BASE64_STANDARD}; use entropy_protocol::RecoverableSignature; use entropy_shared::HashingAlgorithm; -use futures::{future, stream::StreamExt}; +use futures::stream::StreamExt; use sp_core::{sr25519, Pair}; use subxt::{ backend::legacy::LegacyRpcMethods, + ext::sp_core::sr25519::Signature, utils::{AccountId32 as SubxtAccountId32, H256}, Config, OnlineClient, }; @@ -113,14 +115,13 @@ pub async fn sign( ) -> Result { let message_hash = Hasher::keccak(&message); - let validators_info = get_signers_from_chain(api, rpc).await?; + let validators_info = get_validators_not_signer_for_relay(api, rpc).await?; tracing::debug!("Validators info {:?}", validators_info); let block_number = rpc.chain_get_header(None).await?.ok_or(ClientError::BlockNumber)?.number; let signature_request = UserSignatureRequest { message: hex::encode(message), auxilary_data: Some(vec![auxilary_data.map(hex::encode)]), - validators_info: validators_info.clone(), block_number, hash: HashingAlgorithm::Keccak, signature_verifying_key: signature_verifying_key.to_vec(), @@ -129,70 +130,64 @@ pub async fn sign( let signature_request_vec = serde_json::to_vec(&signature_request)?; let client = reqwest::Client::new(); - // Make http requests to TSS servers - let submit_transaction_requests = validators_info - .iter() - .map(|validator_info| async { - let encrypted_message = EncryptedSignedMessage::new( - &user_keypair, - signature_request_vec.clone(), - &validator_info.x25519_public_key, - &[], - )?; - let message_json = serde_json::to_string(&encrypted_message)?; - - let url = format!("http://{}/user/sign_tx", validator_info.ip_address); - - let res = client - .post(url) - .header("Content-Type", "application/json") - .body(message_json) - .send() - .await; - Ok::<_, ClientError>(res) - }) - .collect::>(); - - // If we have a keyshare, connect to TSS servers - let results = future::try_join_all(submit_transaction_requests).await?; - - // Get the first result - if let Some(res) = results.into_iter().next() { - let output = res?; - if output.status() != 200 { - return Err(ClientError::SigningFailed(output.text().await?)); - } - - let mut bytes_stream = output.bytes_stream(); - let chunk = bytes_stream.next().await.ok_or(ClientError::NoResponse)??; - let signing_result: Result<(String, sr25519::Signature), String> = - serde_json::from_slice(&chunk)?; - let (signature_base64, signature_of_signature) = - signing_result.map_err(ClientError::SigningFailed)?; - tracing::debug!("Signature: {}", signature_base64); - let mut decoded_sig = BASE64_STANDARD.decode(signature_base64)?; - - // Verify the response signature from the TSS client - if !sr25519::Pair::verify( + let mut rng = rand::thread_rng(); + let random_index = rng.gen_range(0..validators_info.len()); + let validator_info = &validators_info[random_index]; + + // Make http request to TSS server + let encrypted_message = EncryptedSignedMessage::new( + &user_keypair, + signature_request_vec.clone(), + &validator_info.x25519_public_key, + &[], + )?; + let message_json = serde_json::to_string(&encrypted_message)?; + + let url = format!("http://{}/user/relay_tx", validator_info.ip_address); + + let result = client + .post(url) + .header("Content-Type", "application/json") + .body(message_json) + .send() + .await?; + + let mut bytes_stream = result.bytes_stream(); + let chunk = bytes_stream.next().await.ok_or(ClientError::NoResponse)??; + let signing_results: Vec> = serde_json::from_slice(&chunk)?; + // take only one of the responses randomly + let mut rng = rand::thread_rng(); + let random_index = rng.gen_range(0..signing_results.len()); + let (signature_base64, signature_of_signature) = + signing_results[random_index].clone().map_err(ClientError::SigningFailed)?; + tracing::debug!("Signature: {}", signature_base64); + let mut decoded_sig = BASE64_STANDARD.decode(signature_base64)?; + + // Verify the response signature from the TSS client + let signers = get_all_signers_from_chain(api, rpc).await?; + let mut sig_recovery_results = vec![]; + for signer_info in signers { + let sig_recovery = ::verify( &signature_of_signature, - &decoded_sig, - &sr25519::Public(validators_info[0].tss_account.0), - ) { - return Err(ClientError::BadSignature); - } + decoded_sig.clone(), + &sr25519::Public(signer_info.tss_account.0), + ); + sig_recovery_results.push(sig_recovery) + } - let recovery_digit = decoded_sig.pop().ok_or(ClientError::NoRecoveryId)?; - let signature = k256Signature::from_slice(&decoded_sig)?; - let recovery_id = - RecoveryId::from_byte(recovery_digit).ok_or(ClientError::BadRecoveryId)?; + if !sig_recovery_results.contains(&true) { + return Err(ClientError::BadSignature); + } - let verifying_key_of_signature = - VerifyingKey::recover_from_prehash(&message_hash, &signature, recovery_id)?; - tracing::debug!("Verifying Key {:?}", verifying_key_of_signature); + let recovery_digit = decoded_sig.pop().ok_or(ClientError::NoRecoveryId)?; + let signature = k256Signature::from_slice(&decoded_sig)?; + let recovery_id = RecoveryId::from_byte(recovery_digit).ok_or(ClientError::BadRecoveryId)?; - return Ok(RecoverableSignature { signature, recovery_id }); - } - Err(ClientError::NoResponse) + let verifying_key_of_signature = + VerifyingKey::recover_from_prehash(&message_hash, &signature, recovery_id)?; + tracing::debug!("Verifying Key {:?}", verifying_key_of_signature); + + return Ok(RecoverableSignature { signature, recovery_id }); } /// Store a program on chain and return it's hash diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 1c018fad2..9b4aa101b 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -30,8 +30,6 @@ pub struct UserSignatureRequest { pub message: String, /// Hex-encoded auxilary data for program evaluation, will not be signed (eg. zero-knowledge proof, serialized struct, etc) pub auxilary_data: Option>>, - /// Information from the validators in signing party - pub validators_info: Vec, /// When the message was created and signed pub block_number: BlockNumber, /// Hashing algorithm to be used for signing @@ -40,24 +38,32 @@ pub struct UserSignatureRequest { pub signature_verifying_key: Vec, } -pub async fn get_signers_from_chain( +/// Represents an unparsed transaction request coming from a relayer to a signer. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RelayerSignatureRequest { + // Request relayed from user to signer + pub user_signature_request: UserSignatureRequest, + /// Information for the validators in the signing party + pub validators_info: Vec, +} + +/// Gets a validator from chain to relay a message to the signers +/// Filters out all signers +pub async fn get_validators_not_signer_for_relay( api: &OnlineClient, rpc: &LegacyRpcMethods, ) -> Result, SubgroupGetError> { let signer_query = entropy::storage().staking_extension().signers(); - let mut validators = query_chain(api, rpc, signer_query, None) + let signers = query_chain(api, rpc, signer_query, None) .await? .ok_or_else(|| SubgroupGetError::ChainFetch("Get all validators error"))?; - let key_info_query = entropy::storage().parameters().signers_info(); - let threshold = query_chain(api, rpc, key_info_query, None) + let validators_query = entropy::storage().session().validators(); + let mut validators = query_chain(api, rpc, validators_query, None) .await? - .ok_or_else(|| SubgroupGetError::ChainFetch("Failed to get signers info"))? - .threshold; - - // TODO #899 For now we just take the first t validators as the ones to perform signing - validators.truncate(threshold as usize); + .ok_or_else(|| SubgroupGetError::ChainFetch("Error getting validators"))?; + validators.retain(|validator| !signers.contains(validator)); let block_hash = rpc.chain_get_block_hash(None).await?; let mut handles = Vec::new(); @@ -85,6 +91,51 @@ pub async fn get_signers_from_chain( handles.push(handle); } + let mut all_validators: Vec = vec![]; + for handle in handles { + all_validators.push(handle.await??); + } + + Ok(all_validators) +} + +/// Gets all signers from chain +pub async fn get_all_signers_from_chain( + api: &OnlineClient, + rpc: &LegacyRpcMethods, +) -> Result, SubgroupGetError> { + let signer_query = entropy::storage().staking_extension().signers(); + let signers = query_chain(api, rpc, signer_query, None) + .await? + .ok_or_else(|| SubgroupGetError::ChainFetch("Get all validators error"))?; + + let block_hash = rpc.chain_get_block_hash(None).await?; + let mut handles = Vec::new(); + + for signer in signers { + 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(signer); + let server_info = query_chain(&api, &rpc, threshold_address_query, block_hash) + .await? + .ok_or_else(|| { + SubgroupGetError::ChainFetch("threshold_servers query error") + })?; + 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??); diff --git a/crates/testing-utils/keyshares/production/dave-keyshare-held-by-alice.keyshare b/crates/testing-utils/keyshares/production/dave-keyshare-held-by-alice.keyshare new file mode 100644 index 000000000..1e3aedf1d Binary files /dev/null and b/crates/testing-utils/keyshares/production/dave-keyshare-held-by-alice.keyshare differ diff --git a/crates/testing-utils/keyshares/production/dave-keyshare-held-by-bob.keyshare b/crates/testing-utils/keyshares/production/dave-keyshare-held-by-bob.keyshare new file mode 100644 index 000000000..2176adf50 Binary files /dev/null and b/crates/testing-utils/keyshares/production/dave-keyshare-held-by-bob.keyshare differ diff --git a/crates/testing-utils/keyshares/production/dave-keyshare-held-by-dave.keyshare b/crates/testing-utils/keyshares/production/dave-keyshare-held-by-dave.keyshare new file mode 100644 index 000000000..24c317770 Binary files /dev/null and b/crates/testing-utils/keyshares/production/dave-keyshare-held-by-dave.keyshare differ diff --git a/crates/testing-utils/keyshares/production/eve-keyshare-held-by-alice.keyshare b/crates/testing-utils/keyshares/production/eve-keyshare-held-by-alice.keyshare new file mode 100644 index 000000000..c9ff36c6e Binary files /dev/null and b/crates/testing-utils/keyshares/production/eve-keyshare-held-by-alice.keyshare differ diff --git a/crates/testing-utils/keyshares/production/eve-keyshare-held-by-bob.keyshare b/crates/testing-utils/keyshares/production/eve-keyshare-held-by-bob.keyshare new file mode 100644 index 000000000..47276c3c8 Binary files /dev/null and b/crates/testing-utils/keyshares/production/eve-keyshare-held-by-bob.keyshare differ diff --git a/crates/testing-utils/keyshares/production/eve-keyshare-held-by-dave.keyshare b/crates/testing-utils/keyshares/production/eve-keyshare-held-by-dave.keyshare new file mode 100644 index 000000000..8f8d8a98e Binary files /dev/null and b/crates/testing-utils/keyshares/production/eve-keyshare-held-by-dave.keyshare differ diff --git a/crates/testing-utils/keyshares/test/dave-keyshare-held-by-alice.keyshare b/crates/testing-utils/keyshares/test/dave-keyshare-held-by-alice.keyshare new file mode 100644 index 000000000..bc2be11ab Binary files /dev/null and b/crates/testing-utils/keyshares/test/dave-keyshare-held-by-alice.keyshare differ diff --git a/crates/testing-utils/keyshares/test/dave-keyshare-held-by-bob.keyshare b/crates/testing-utils/keyshares/test/dave-keyshare-held-by-bob.keyshare new file mode 100644 index 000000000..f1e36e8c1 Binary files /dev/null and b/crates/testing-utils/keyshares/test/dave-keyshare-held-by-bob.keyshare differ diff --git a/crates/testing-utils/keyshares/test/dave-keyshare-held-by-charlie.keyshare b/crates/testing-utils/keyshares/test/dave-keyshare-held-by-charlie.keyshare new file mode 100644 index 000000000..b9cdbc7a2 Binary files /dev/null and b/crates/testing-utils/keyshares/test/dave-keyshare-held-by-charlie.keyshare differ diff --git a/crates/testing-utils/keyshares/test/eve-keyshare-held-by-alice.keyshare b/crates/testing-utils/keyshares/test/eve-keyshare-held-by-alice.keyshare new file mode 100644 index 000000000..4281330bc Binary files /dev/null and b/crates/testing-utils/keyshares/test/eve-keyshare-held-by-alice.keyshare differ diff --git a/crates/testing-utils/keyshares/test/eve-keyshare-held-by-bob.keyshare b/crates/testing-utils/keyshares/test/eve-keyshare-held-by-bob.keyshare new file mode 100644 index 000000000..0225f679b Binary files /dev/null and b/crates/testing-utils/keyshares/test/eve-keyshare-held-by-bob.keyshare differ diff --git a/crates/testing-utils/keyshares/test/eve-keyshare-held-by-charlie.keyshare b/crates/testing-utils/keyshares/test/eve-keyshare-held-by-charlie.keyshare new file mode 100644 index 000000000..c5537a9d5 Binary files /dev/null and b/crates/testing-utils/keyshares/test/eve-keyshare-held-by-charlie.keyshare differ diff --git a/crates/testing-utils/src/create_test_keyshares.rs b/crates/testing-utils/src/create_test_keyshares.rs index c3bf53d6b..7b80de562 100644 --- a/crates/testing-utils/src/create_test_keyshares.rs +++ b/crates/testing-utils/src/create_test_keyshares.rs @@ -38,18 +38,19 @@ where Params: SchemeParams, { let signing_key = SigningKey::from_bytes(&(distributed_secret_key_bytes).into()).unwrap(); - let signers = vec![alice.clone(), bob, charlie.clone()]; + let signers = vec![alice.clone(), bob.clone(), charlie.clone()]; let session_id = SessionId::from_seed(b"12345".as_slice()); let all_parties = signers.iter().map(|pair| PartyId::from(pair.public())).collect::>(); - let old_holders = all_parties.clone().into_iter().take(2).collect::>(); + let mut old_holders = all_parties.clone(); + old_holders.remove(&PartyId::from(charlie.clone().public())); let keyshares = KeyShare::::new_centralized(&mut OsRng, &old_holders, Some(&signing_key)); let aux_infos = AuxInfo::::new_centralized(&mut OsRng, &all_parties); - let alice_id = PartyId::from(alice.public()); + let alice_id = PartyId::from(bob.public()); let new_holder = NewHolder { verifying_key: keyshares[&alice_id].verifying_key(), old_threshold: 2, diff --git a/crates/threshold-signature-server/src/helpers/signing.rs b/crates/threshold-signature-server/src/helpers/signing.rs index ba8856d5c..609dde2f2 100644 --- a/crates/threshold-signature-server/src/helpers/signing.rs +++ b/crates/threshold-signature-server/src/helpers/signing.rs @@ -17,7 +17,7 @@ pub use entropy_client::Hasher; use std::time::Duration; -use entropy_client::user::UserSignatureRequest; +use entropy_client::user::RelayerSignatureRequest; use entropy_protocol::{Listener, RecoverableSignature, SessionId, SigningSessionInfo}; use entropy_shared::SETUP_TIMEOUT_SECONDS; use sp_core::Pair; @@ -41,7 +41,7 @@ use crate::{ #[tracing::instrument(skip(app_state), level = tracing::Level::DEBUG)] pub async fn do_signing( rpc: &LegacyRpcMethods, - user_signature_request: UserSignatureRequest, + relayer_signature_request: RelayerSignatureRequest, app_state: &AppState, signing_session_info: SigningSessionInfo, request_limit: u32, @@ -52,7 +52,7 @@ pub async fn do_signing( let state = &app_state.listener_state; let kv_manager = &app_state.kv_store; - let info = SignInit::new(user_signature_request.clone(), signing_session_info.clone()); + let info = SignInit::new(relayer_signature_request.clone(), signing_session_info.clone()); let signing_service = ThresholdSigningService::new(state, kv_manager); let (pair_signer, x25519_secret_key) = get_signer_and_x25519_secret(kv_manager) .await @@ -64,7 +64,7 @@ pub async fn do_signing( // Set up context for signing protocol execution let sign_context = signing_service.get_sign_context(info.clone(), derivation_path).await?; - let tss_accounts: Vec = user_signature_request + let tss_accounts: Vec = relayer_signature_request .validators_info .iter() .map(|validator_info| validator_info.tss_account.clone()) @@ -72,7 +72,7 @@ pub async fn do_signing( // subscribe to all other participating parties. Listener waits for other subscribers. let (rx_ready, rx_from_others, listener) = - Listener::new(user_signature_request.validators_info, &account_id); + Listener::new(relayer_signature_request.validators_info, &account_id); let session_id = SessionId::Sign(sign_context.sign_init.signing_session_info.clone()); diff --git a/crates/threshold-signature-server/src/helpers/substrate.rs b/crates/threshold-signature-server/src/helpers/substrate.rs index 8a937b249..a23da6503 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::{ }; pub use entropy_client::substrate::{query_chain, submit_transaction}; use entropy_shared::user::ValidatorInfo; +use rand::prelude::SliceRandom; use subxt::{backend::legacy::LegacyRpcMethods, utils::AccountId32, Config, OnlineClient}; /// Given a threshold server's account ID, return its corresponding stash (validator) address. @@ -108,3 +109,59 @@ pub async fn get_validators_info( } Ok(all_signers) } + +/// Returns a threshold of signer's ValidatorInfo from the chain +pub async fn get_signers_from_chain( + api: &OnlineClient, + rpc: &LegacyRpcMethods, +) -> Result<(Vec, Vec), UserErr> { + let signer_query = entropy::storage().staking_extension().signers(); + let signers = query_chain(api, rpc, signer_query, None) + .await? + .ok_or_else(|| UserErr::ChainFetch("Get all validators error"))?; + + let key_info_query = entropy::storage().parameters().signers_info(); + let threshold = query_chain(api, rpc, key_info_query, None) + .await? + .ok_or_else(|| UserErr::ChainFetch("Failed to get signers info"))? + .threshold; + + let selected_signers: Vec<_> = { + let cloned_signers = signers.clone(); + cloned_signers + .choose_multiple(&mut rand::thread_rng(), threshold as usize) + .cloned() + .collect() + }; + + let block_hash = rpc.chain_get_block_hash(None).await?; + let mut handles = Vec::new(); + + for signer in selected_signers { + 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(signer); + let server_info = query_chain(&api, &rpc, threshold_address_query, block_hash) + .await? + .ok_or_else(|| UserErr::ChainFetch("threshold_servers query error"))?; + 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_selected_signers: Vec = vec![]; + for handle in handles { + all_selected_signers.push(handle.await??); + } + + Ok((all_selected_signers, signers)) +} diff --git a/crates/threshold-signature-server/src/helpers/tests.rs b/crates/threshold-signature-server/src/helpers/tests.rs index 172f9ab31..b523072ab 100644 --- a/crates/threshold-signature-server/src/helpers/tests.rs +++ b/crates/threshold-signature-server/src/helpers/tests.rs @@ -210,14 +210,22 @@ pub async fn spawn_testing_validators( } /// Add the pre-generated test keyshares to a kvdb -async fn put_keyshares_in_db(index: usize, validator_name: ValidatorName) { +async fn put_keyshares_in_db(_index: usize, validator_name: ValidatorName) { // Eve's keyshares are used as the network parent key let user_name = "eve"; + + let string_validator_name = match validator_name { + ValidatorName::Alice => "alice", + ValidatorName::Bob => "bob", + ValidatorName::Charlie => "charlie", + ValidatorName::Dave => "dave", + ValidatorName::Eve => "eve", + }; let keyshare_bytes = { let project_root = project_root::get_project_root().expect("Error obtaining project root."); let file_path = project_root.join(format!( - "crates/testing-utils/keyshares/production/{}-keyshare-{}.keyshare", - user_name, index + "crates/testing-utils/keyshares/production/{}-keyshare-held-by-{}.keyshare", + user_name, string_validator_name )); std::fs::read(file_path).unwrap() }; @@ -278,12 +286,13 @@ pub async fn jump_start_network_with_signer( api: &OnlineClient, rpc: &LegacyRpcMethods, signer: &PairSigner, -) { +) -> Option { 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, ValidatorName::Charlie, ValidatorName::Dave]; + let mut non_signer = None; let mut keyshare_index = 0; for validator_name in validators_names { let mnemonic = development_mnemonic(&Some(validator_name)); @@ -299,8 +308,11 @@ pub async fn jump_start_network_with_signer( { put_keyshares_in_db(keyshare_index, validator_name).await; keyshare_index += 1; + } else { + non_signer = Some(validator_name); } } + non_signer } /// Helper to store a program and register a user. Returns the verify key and program hash. diff --git a/crates/threshold-signature-server/src/lib.rs b/crates/threshold-signature-server/src/lib.rs index ea1ed07d9..960ccd47d 100644 --- a/crates/threshold-signature-server/src/lib.rs +++ b/crates/threshold-signature-server/src/lib.rs @@ -32,17 +32,57 @@ //! is an encrypted, signed message. //! //! -//! #### `/user/sign_tx` - POST +//! #### `/user/relay_tx` - POST //! -//! [crate::user::api::sign_tx()] +//! [crate::user::api::relay_tx()] //! -//! Called by a user to submit a transaction to sign (the new way of doing signing). Takes a +//! Called by a user to submit a transaction to sign. Takes a //! [UserSignatureRequest] encrypted in a [SignedMessage](crate::validation::SignedMessage). //! +//! Picks signers and gets them to sign a message then returns the responses to the user. +//! //! The response is chunked response stream. If the `UserSignatureRequest` could be processed, a //! success response header is sent. Then the signing protocol runs. When the it finishes, a single //! message will be sent on the response stream with the result. //! +//! If everything went well, the message will be a vector of JSON objects with a signle property "Ok" +//! containing an array which contains two strings. Each element in the vector is a response from a signer. +//! +//! For example: +//! +//! `[{"Ok":["t7Mcxfdigds3RoT6OO/P+uMFE+XigRjUpn72E1cRU4Q2u7cVxZlsNRYhnahA+DvSNHBddj0HRz5u/XPlJT9QOQE=","32d7c0bfd90b546993d1ad51c542e1fc9dd1706c7bca395c8bd7f9642ae842400769488404dabd25d438cf08785a6750f95e7489245b8760af115f450d5f0a83"]}]` +//! +//! The first string is a base64 encoded signature produced by the signing protocol. This is a 65 +//! byte signature, the final byte of which is a +//! [recovery ID](https://docs.rs/synedrion/latest/synedrion/ecdsa/struct.RecoveryId.html). +//! +//! The second string is a hex encoded sr25519 signature of the signature made by the TSS server, +//! which can be used to authenticate that this response really came from this TSS server. +//! +//! In case signing was not successfull, the message will be a JSON object with a signle property "Err" +//! containing an error message, for example: +//! +//! "[{\"Err\":\"Too many requests - wait a block\"},{\"Err\":\"Too many requests - wait a block\"}]" +//! +//! Curl example for `user/sign_tx`: +//! ```text +//! curl -X POST -H "Content-Type: application/json" \ +//! -d '{"msg" "0x174...hex encoded signedmessage...","sig":"821754409744cbb878b44bd1e3dc575a4ea721e12d781b074fcdb808fc79fd33dd1928b1a281c0b6261a30536a7c0106a102f27dad1bc3ef475b626f0e57c983","pk":[172,133,159,138,33,110,235,27,50,11,76,118,209,24,218,61,116,7,250,82,52,132,208,169,128,18,109,59,77,13,34,10],"recip":[10,192,41,240,184,83,178,59,237,101,45,109,13,230,155,124,195,141,148,249,55,50,238,252,133,181,134,30,144,247,58,34],"a":[169,94,23,7,19,184,134,70,233,117,2,84,242,135,246,95,159,14,218,125,209,191,175,89,41,196,182,96,117,5,159,98],"nonce":[114,93,158,35,209,188,96,248,85,131,95,237]}' \ +//! -H "Accept: application/json" \ +//! http://127.0.0.1:3001/user/relay_tx +//! ``` +//! +//! #### `/user/sign_tx` - POST +//! +//! [crate::user::api::sign_tx()] +//! +//! Called by a relayer to submit a transaction to sign. Takes a +//! [RelayerSignatureRequest] encrypted in a [SignedMessage](crate::validation::SignedMessage). +//! +//! The response is chunked response stream. If the `RelayerSignatureRequest` could be processed, a +//! success response header is sent. Then the signing protocol runs. When the it finishes, a single +//! message will be sent on the response stream with the result. +//! //! If everything went well, the message will be a JSON object with a signle property "Ok" //! containing an array which contains two strings. //! @@ -169,6 +209,7 @@ pub fn app(app_state: AppState) -> Router { let mut routes = Router::new() .route("/generate_network_key", post(generate_network_key)) .route("/user/sign_tx", post(sign_tx)) + .route("/user/relay_tx", post(relay_tx)) .route("/signer/proactive_refresh", post(proactive_refresh)) .route("/validator/reshare", post(new_reshare)) .route("/validator/rotate_network_key", post(rotate_network_key)) diff --git a/crates/threshold-signature-server/src/sign_init.rs b/crates/threshold-signature-server/src/sign_init.rs index e73910ae9..b8d98f5cf 100644 --- a/crates/threshold-signature-server/src/sign_init.rs +++ b/crates/threshold-signature-server/src/sign_init.rs @@ -17,7 +17,7 @@ use entropy_protocol::{SigningSessionInfo, ValidatorInfo}; use serde::{Deserialize, Serialize}; -use entropy_client::user::UserSignatureRequest; +use entropy_client::user::RelayerSignatureRequest; /// Information passed to the Signing Client, to initiate the signing process. /// Most of this information comes from a `Message` struct which gets propagated when a user's @@ -33,7 +33,7 @@ pub struct SignInit { impl SignInit { /// Creates new signing object based on passed in data #[allow(dead_code)] - pub fn new(message: UserSignatureRequest, signing_session_info: SigningSessionInfo) -> Self { + pub fn new(message: RelayerSignatureRequest, signing_session_info: SigningSessionInfo) -> Self { Self { signing_session_info, validators_info: message.validators_info } } } diff --git a/crates/threshold-signature-server/src/user/api.rs b/crates/threshold-signature-server/src/user/api.rs index 2b779be3e..f6da63a1b 100644 --- a/crates/threshold-signature-server/src/user/api.rs +++ b/crates/threshold-signature-server/src/user/api.rs @@ -13,13 +13,17 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use std::{str::FromStr, sync::Arc, time::SystemTime}; +use std::{ + str::{from_utf8, FromStr}, + sync::Arc, + time::SystemTime, +}; use axum::{ body::{Body, Bytes}, extract::State, http::StatusCode, - response::IntoResponse, + response::{IntoResponse, Response}, routing::{get, post}, Json, Router, }; @@ -40,8 +44,9 @@ use entropy_shared::{ }; use futures::{ channel::mpsc, - future::{join_all, FutureExt}, - Stream, + future::{join_all, try_join_all, FutureExt}, + stream::TryStreamExt, + Stream, StreamExt, }; use num::{bigint::BigInt, FromPrimitive, Num, ToPrimitive}; use parity_scale_codec::{Decode, DecodeAll, Encode}; @@ -62,13 +67,15 @@ use x25519_dalek::StaticSecret; use zeroize::Zeroize; use super::{ParsedUserInputPartyInfo, ProgramError, UserErr, UserInputPartyInfo}; +use crate::chain_api::entropy::runtime_types::pallet_registry::pallet::RegisteredInfo; use crate::{ chain_api::{entropy, get_api, get_rpc, EntropyConfig}, helpers::{ launch::LATEST_BLOCK_NUMBER_NEW_USER, signing::{do_signing, Hasher}, substrate::{ - get_oracle_data, get_program, get_stash_address, query_chain, submit_transaction, + get_oracle_data, get_program, get_signers_from_chain, get_stash_address, + get_validators_info, query_chain, submit_transaction, }, user::{check_in_registration_group, compute_hash, do_dkg}, validator::{get_signer, get_signer_and_x25519_secret}, @@ -79,7 +86,7 @@ use crate::{ AppState, Configuration, }; -pub use entropy_client::user::{get_signers_from_chain, UserSignatureRequest}; +pub use entropy_client::user::{RelayerSignatureRequest, UserSignatureRequest}; pub const REQUEST_KEY_HEADER: &str = "REQUESTS"; /// Type for validators to send user key's back and forth @@ -106,6 +113,137 @@ pub struct RequestLimitStorage { /// Called by a user to initiate the signing process for a message /// /// Takes an [EncryptedSignedMessage] containing a JSON serialized [UserSignatureRequest] +/// +/// Chooses signers and relays transactions to them and then results back to user +#[tracing::instrument(skip_all, fields(request_author))] +pub async fn relay_tx( + State(app_state): State, + Json(encrypted_msg): Json, +) -> Result<(StatusCode, Body), UserErr> { + let (signer, x25519_secret) = get_signer_and_x25519_secret(&app_state.kv_store).await?; + let api = get_api(&app_state.configuration.endpoint).await?; + let rpc = get_rpc(&app_state.configuration.endpoint).await?; + + // make sure is a validator and not a signer + let validators_query = entropy::storage().session().validators(); + let validators = query_chain(&api, &rpc, validators_query, None) + .await? + .ok_or_else(|| UserErr::ChainFetch("Error getting validators"))?; + + let validators_info = get_validators_info(&api, &rpc, validators).await?; + + validators_info + .iter() + .find(|validator| validator.tss_account == *signer.account_id()) + .ok_or_else(|| UserErr::NotValidator)?; + + let (selected_signers, all_signers) = get_signers_from_chain(&api, &rpc).await?; + + let signers_info = get_validators_info(&api, &rpc, all_signers).await?; + + signers_info + .iter() + .find(|signer_info| signer_info.tss_account == *signer.account_id()) + .map_or(Ok(()), |_| Err(UserErr::RelayMessageSigner))?; + + let signed_message = encrypted_msg.decrypt(&x25519_secret, &[])?; + + tracing::Span::current().record("request_author", signed_message.account_id().to_string()); + + let user_signature_request: UserSignatureRequest = + serde_json::from_slice(&signed_message.message.0)?; + let relayer_sig_req = + RelayerSignatureRequest { user_signature_request, validators_info: selected_signers }; + let block_number = rpc + .chain_get_header(None) + .await? + .ok_or_else(|| UserErr::OptionUnwrapError("Error Getting Block Number".to_string()))? + .number; + + let string_verifying_key = + hex::encode(relayer_sig_req.user_signature_request.signature_verifying_key.clone()); + + let _ = pre_sign_checks( + &api, + &rpc, + relayer_sig_req.user_signature_request.clone(), + block_number, + string_verifying_key, + ) + .await?; + + // relay message + let (mut response_tx, response_rx) = mpsc::channel(1); + + tokio::spawn(async move { + let result: Result<(), UserErr> = async { + let client = reqwest::Client::new(); + let results = join_all( + relayer_sig_req + .validators_info + .iter() + .map(|signer_info| async { + let signed_message = EncryptedSignedMessage::new( + signer.signer(), + serde_json::to_vec(&relayer_sig_req.clone())?, + &signer_info.x25519_public_key, + &[], + )?; + + let url = format!("http://{}/user/sign_tx", signer_info.ip_address.clone()); + + let response = client + .post(url) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&signed_message)?) + .send() + .await?; + + Ok::<_, UserErr>(response) + }) + .collect::>(), + ) + .await; + + let mut send_back = vec![]; + + for result in results { + let mut resp = result?; + let chunk = resp + .chunk() + .await? + .ok_or(UserErr::OptionUnwrapError("No chunk data".to_string()))?; + + if resp.status() == 200 { + let signing_result: Result<(String, Signature), String> = + serde_json::from_slice(&chunk)?; + send_back.push(signing_result); + } else { + send_back.push(Err(String::from_utf8(chunk.to_vec())?)); + } + } + + if response_tx.try_send(serde_json::to_string(&send_back)?).is_err() { + tracing::warn!("Cannot send signing protocol output - connection is closed"); + } + + Ok(()) + } + .await; + + if let Err(e) = result { + tracing::error!("Error in tokio::spawn task: {:?}", e); + } + }); + + let result_stream = response_rx.map(Ok::<_, UserErr>); + + Ok((StatusCode::OK, Body::from_stream(result_stream))) +} + +/// Called by a relayer to initiate the signing process for a message +/// +/// Takes an [EncryptedSignedMessage] containing a JSON serialized [RelayerSignatureRequest] #[tracing::instrument(skip_all, fields(request_author))] pub async fn sign_tx( State(app_state): State, @@ -120,86 +258,92 @@ pub async fn sign_tx( let request_author = SubxtAccountId32(*signed_message.account_id().as_ref()); tracing::Span::current().record("request_author", signed_message.account_id().to_string()); + let validators_query = entropy::storage().session().validators(); + + let validators = query_chain(&api, &rpc, validators_query, None) + .await? + .ok_or_else(|| UserErr::ChainFetch("Error getting signers"))?; + + let validators_info = get_validators_info(&api, &rpc, validators).await?; + + validators_info + .iter() + .find(|validator| validator.tss_account == request_author) + .ok_or_else(|| UserErr::NotRelayedFromValidator)?; let request_limit_query = entropy::storage().parameters().request_limit(); let request_limit = query_chain(&api, &rpc, request_limit_query, None) .await? .ok_or_else(|| UserErr::ChainFetch("Failed to get request limit"))?; - let mut user_sig_req: UserSignatureRequest = serde_json::from_slice(&signed_message.message.0)?; + let relayer_sig_request: RelayerSignatureRequest = + serde_json::from_slice(&signed_message.message.0)?; - let string_verifying_key = hex::encode(user_sig_req.signature_verifying_key.clone()); - request_limit_check(&rpc, &app_state.kv_store, string_verifying_key.clone(), request_limit) - .await?; - - let block_number = rpc - .chain_get_header(None) + // check validator info > threshold + let key_info_query = entropy::storage().parameters().signers_info(); + let threshold = query_chain(&api, &rpc, key_info_query, None) .await? - .ok_or_else(|| UserErr::OptionUnwrapError("Error Getting Block Number".to_string()))? - .number; + .ok_or_else(|| UserErr::ChainFetch("Failed to get signers info"))? + .threshold; - check_stale(user_sig_req.block_number, block_number).await?; - - // Probably impossible but block signing from parent key anyways - if string_verifying_key == hex::encode(NETWORK_PARENT_KEY) { - return Err(UserErr::NoSigningFromParentKey); + if relayer_sig_request.validators_info.len() < threshold as usize { + return Err(UserErr::TooFewSigners); } + // validators are signers + let signer_query = entropy::storage().staking_extension().signers(); + let signers = query_chain(&api, &rpc, signer_query, None) + .await? + .ok_or_else(|| UserErr::ChainFetch("Get all validators error"))?; - let user_details = - get_registered_details(&api, &rpc, user_sig_req.signature_verifying_key.clone()).await?; - check_hash_pointer_out_of_bounds(&user_sig_req.hash, user_details.programs_data.0.len())?; + let validator_exists = { + let mut found = true; - let message = hex::decode(&user_sig_req.message)?; + for validator_info in &relayer_sig_request.validators_info { + let stash_address_query = entropy::storage() + .staking_extension() + .threshold_to_stash(validator_info.tss_account.clone()); - if user_details.programs_data.0.is_empty() { - return Err(UserErr::NoProgramPointerDefined()); - } + let stash_address = query_chain(&api, &rpc, stash_address_query, None) + .await? + .ok_or_else(|| UserErr::ChainFetch("Stash Fetch Error"))?; - // Handle aux data padding, if it is not explicit by client for ease send through None, error - // if incorrect length - let auxilary_data_vec; - if let Some(auxilary_data) = user_sig_req.clone().auxilary_data { - if auxilary_data.len() < user_details.programs_data.0.len() { - return Err(UserErr::MismatchAuxData); - } else { - auxilary_data_vec = auxilary_data; + // If the stash_address is found in signers, we can stop further checking + if !signers.contains(&stash_address) { + found = false; + break; + } } - } else { - auxilary_data_vec = vec![None; user_details.programs_data.0.len()]; - } - - // gets fuel from chain - let max_instructions_per_programs_query = - entropy::storage().parameters().max_instructions_per_programs(); - let fuel = query_chain(&api, &rpc, max_instructions_per_programs_query, None) - .await? - .ok_or_else(|| UserErr::ChainFetch("Max instructions per program error"))?; - - let mut runtime = Runtime::new(ProgramConfig { fuel }); + found + }; - for (i, program_data) in user_details.programs_data.0.iter().enumerate() { - let program_info = get_program(&api, &rpc, &program_data.program_pointer).await?; - let oracle_data = get_oracle_data(&api, &rpc, program_info.oracle_data_pointer).await?; - let auxilary_data = auxilary_data_vec[i].as_ref().map(hex::decode).transpose()?; - let signature_request = SignatureRequest { message: message.clone(), auxilary_data }; - runtime.evaluate( - &program_info.bytecode, - &signature_request, - Some(&program_data.program_config), - Some(&oracle_data), - )?; + if !validator_exists { + return Err(UserErr::IncorrectSigner); } - let signers = get_signers_from_chain(&api, &rpc).await?; + let string_verifying_key = + hex::encode(relayer_sig_request.user_signature_request.signature_verifying_key.clone()); + request_limit_check(&rpc, &app_state.kv_store, string_verifying_key.clone(), request_limit) + .await?; + + let block_number = rpc + .chain_get_header(None) + .await? + .ok_or_else(|| UserErr::OptionUnwrapError("Error Getting Block Number".to_string()))? + .number; - // Use the validator info from chain as we can be sure it is in the correct order and the - // details are correct - user_sig_req.validators_info = signers; + let (mut runtime, user_details, message) = pre_sign_checks( + &api, + &rpc, + relayer_sig_request.user_signature_request.clone(), + block_number, + string_verifying_key, + ) + .await?; let message_hash = compute_hash( &api, &rpc, - &user_sig_req.hash, + &relayer_sig_request.user_signature_request.hash, &mut runtime, &user_details.programs_data.0, message.as_slice(), @@ -207,7 +351,10 @@ pub async fn sign_tx( .await?; let signing_session_id = SigningSessionInfo { - signature_verifying_key: user_sig_req.signature_verifying_key.clone(), + signature_verifying_key: relayer_sig_request + .user_signature_request + .signature_verifying_key + .clone(), message_hash, request_author, }; @@ -227,7 +374,7 @@ pub async fn sign_tx( tokio::spawn(async move { let signing_protocol_output = do_signing( &rpc, - user_sig_req, + relayer_sig_request, &app_state, signing_session_id, request_limit, @@ -524,3 +671,63 @@ pub fn check_hash_pointer_out_of_bounds( _ => Ok(()), } } + +pub async fn pre_sign_checks( + api: &OnlineClient, + rpc: &LegacyRpcMethods, + user_sig_req: UserSignatureRequest, + block_number: u32, + string_verifying_key: String, +) -> Result<(Runtime, RegisteredInfo, Vec), UserErr> { + check_stale(user_sig_req.block_number, block_number).await?; + + // Probably impossible but block signing from parent key anyways + if string_verifying_key == hex::encode(NETWORK_PARENT_KEY) { + return Err(UserErr::NoSigningFromParentKey); + } + + let user_details = + get_registered_details(api, rpc, user_sig_req.signature_verifying_key.clone()).await?; + check_hash_pointer_out_of_bounds(&user_sig_req.hash, user_details.programs_data.0.len())?; + + let message = hex::decode(&user_sig_req.message)?; + + if user_details.programs_data.0.is_empty() { + return Err(UserErr::NoProgramPointerDefined()); + } + + // Handle aux data padding, if it is not explicit by client for ease send through None, error + // if incorrect length + let auxilary_data_vec = if let Some(auxilary_data) = user_sig_req.clone().auxilary_data { + if auxilary_data.len() < user_details.programs_data.0.len() { + return Err(UserErr::MismatchAuxData); + } + auxilary_data + } else { + vec![None; user_details.programs_data.0.len()] + }; + + // gets fuel from chain + let max_instructions_per_programs_query = + entropy::storage().parameters().max_instructions_per_programs(); + let fuel = query_chain(api, rpc, max_instructions_per_programs_query, None) + .await? + .ok_or_else(|| UserErr::ChainFetch("Max instructions per program error"))?; + + let mut runtime = Runtime::new(ProgramConfig { fuel }); + + for (i, program_data) in user_details.programs_data.0.iter().enumerate() { + let program_info = get_program(api, rpc, &program_data.program_pointer).await?; + let oracle_data = get_oracle_data(api, rpc, program_info.oracle_data_pointer).await?; + let auxilary_data = auxilary_data_vec[i].as_ref().map(hex::decode).transpose()?; + let signature_request = SignatureRequest { message: message.clone(), auxilary_data }; + runtime.evaluate( + &program_info.bytecode, + &signature_request, + Some(&program_data.program_config), + Some(&oracle_data), + )?; + } + + Ok((runtime, user_details, message)) +} diff --git a/crates/threshold-signature-server/src/user/errors.rs b/crates/threshold-signature-server/src/user/errors.rs index ada849df5..700df06dc 100644 --- a/crates/threshold-signature-server/src/user/errors.rs +++ b/crates/threshold-signature-server/src/user/errors.rs @@ -173,6 +173,16 @@ pub enum UserErr { UnknownHashingAlgorithm, #[error("Failed to derive BIP-32 account: {0}")] Bip32DerivationError(#[from] bip32::Error), + #[error("Message sent directly to signer")] + NotRelayedFromValidator, + #[error("Message not sent to a validator")] + NotValidator, + #[error("Relay message can not be sent to a signer")] + RelayMessageSigner, + #[error("Too few signers selected")] + TooFewSigners, + #[error("Non signer sent from relayer")] + IncorrectSigner, } impl From for UserErr { diff --git a/crates/threshold-signature-server/src/user/tests.rs b/crates/threshold-signature-server/src/user/tests.rs index 6dc2866f0..d82369f90 100644 --- a/crates/threshold-signature-server/src/user/tests.rs +++ b/crates/threshold-signature-server/src/user/tests.rs @@ -21,7 +21,7 @@ use entropy_client::substrate::get_registered_details; use entropy_client::{ client as test_client, client::{sign, update_programs}, - user::get_signers_from_chain, + user::get_all_signers_from_chain, }; use entropy_kvdb::{ clean_tests, @@ -72,6 +72,7 @@ use sp_keyring::{AccountKeyring, Sr25519Keyring}; use std::{ env, fs, path::PathBuf, + str, str::FromStr, sync::Arc, time::{Duration, SystemTime}, @@ -120,7 +121,7 @@ use crate::{ DEFAULT_ENDPOINT, DEFAULT_MNEMONIC, }, signing::Hasher, - substrate::{get_oracle_data, query_chain, submit_transaction}, + substrate::{get_oracle_data, get_signers_from_chain, query_chain, submit_transaction}, tests::{ create_clients, initialize_test_logger, jump_start_network_with_signer, remove_program, run_to_block, setup_client, spawn_testing_validators, store_program_and_register, @@ -134,7 +135,8 @@ use crate::{ user::{ api::{ check_hash_pointer_out_of_bounds, increment_or_wipe_request_limit, request_limit_check, - request_limit_key, RequestLimitStorage, UserRegistrationInfo, UserSignatureRequest, + request_limit_key, RelayerSignatureRequest, RequestLimitStorage, UserRegistrationInfo, + UserSignatureRequest, }, UserErr, }, @@ -170,8 +172,11 @@ async fn test_signature_requests_fail_on_different_conditions() { let one = AccountKeyring::One; let two = AccountKeyring::Two; - let (_validator_ips, _validator_ids) = + let (validator_ips, _validator_ids) = spawn_testing_validators(ChainSpecType::Integration).await; + let mnemonic = development_mnemonic(&Some(ValidatorName::Alice)); + let (tss_signer, _static_secret) = + get_signer_and_x25519_secret_from_mnemonic(&mnemonic.to_string()).unwrap(); // Here we need to use `--chain=integration-tests` and force authoring otherwise we won't be // able to get our chain in the right state to be jump started. @@ -182,41 +187,81 @@ async fn test_signature_requests_fail_on_different_conditions() { // We first need to jump start the network and grab the resulting network wide verifying key // for later - jump_start_network(&entropy_api, &rpc).await; + let non_signer = jump_start_network(&entropy_api, &rpc).await.unwrap(); + let (relayer_ip_and_key, _) = + validator_name_to_relayer_info(non_signer, &entropy_api, &rpc).await; // Register the user with a test program let (verifying_key, program_hash) = store_program_and_register(&entropy_api, &rpc, &one.pair(), &two.pair()).await; // Test: We check that an account with a program succeeds in submiting a signature request - let (validators_info, mut signature_request, validator_ips_and_keys) = + let (_validators_info, mut signature_request, _validator_ips_and_keys) = get_sign_tx_data(&entropy_api, &rpc, hex::encode(PREIMAGE_SHOULD_SUCCEED), verifying_key) .await; // The account we registered does have a program pointer, so this should succeed let test_user_res = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), one) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), one) .await; let message_hash = Hasher::keccak(PREIMAGE_SHOULD_SUCCEED); let decoded_verifying_key = decode_verifying_key(verifying_key.as_slice().try_into().unwrap()).unwrap(); - verify_signature(test_user_res, message_hash, &decoded_verifying_key, &validators_info).await; + + let all_signers_info = get_all_signers_from_chain(&entropy_api, &rpc).await.unwrap(); + verify_signature(test_user_res, message_hash, &decoded_verifying_key, &all_signers_info).await; + + signature_request.block_number = rpc.chain_get_header(None).await.unwrap().unwrap().number; + + let mock_client = reqwest::Client::new(); + + let signed_message = EncryptedSignedMessage::new( + &one.pair(), + serde_json::to_vec(&signature_request.clone()).unwrap(), + &X25519_PUBLIC_KEYS[0], + &[], + ) + .unwrap(); + let url = format!("http://{}/user/sign_tx", validator_ips[0]); + let signature_request_responses_fail_not_relayer = mock_client + .post(url) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&signed_message).unwrap()) + .send() + .await; + assert_eq!( + signature_request_responses_fail_not_relayer.unwrap().text().await.unwrap(), + "Message sent directly to signer" + ); // Test: A user that is not registered is not able to send a signature request signature_request.block_number = rpc.chain_get_header(None).await.unwrap().unwrap().number; signature_request.signature_verifying_key = DEFAULT_VERIFYING_KEY_NOT_REGISTERED.to_vec(); let test_user_res_not_registered = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), two) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), two) .await; - for res in test_user_res_not_registered { - assert_eq!( - res.unwrap().text().await.unwrap(), - "Substrate: User is not registered on-chain" - ); - } + assert_eq!( + test_user_res_not_registered.unwrap().text().await.unwrap(), + "Substrate: User is not registered on-chain" + ); + + let test_user_res_not_registered_sign_tx = submit_transaction_sign_tx_requests( + &entropy_api, + &rpc, + relayer_ip_and_key.clone(), + signature_request.clone(), + tss_signer.signer().clone(), + None, + ) + .await; + + assert_eq!( + test_user_res_not_registered_sign_tx.unwrap().text().await.unwrap(), + "Substrate: User is not registered on-chain" + ); // Test: Signature requests fail if no auxiliary data is set @@ -226,15 +271,28 @@ async fn test_signature_requests_fail_on_different_conditions() { signature_request.auxilary_data = None; let test_user_failed_programs_res = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), one) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), one) .await; - for res in test_user_failed_programs_res { - assert_eq!( - res.unwrap().text().await.unwrap(), + assert_eq!( + test_user_failed_programs_res.unwrap().text().await.unwrap(), "Runtime error: Runtime(Error::Evaluation(\"This program requires that `auxilary_data` be `Some`.\"))" ); - } + + let test_user_failed_programs_res_sign_tx = submit_transaction_sign_tx_requests( + &entropy_api, + &rpc, + relayer_ip_and_key.clone(), + signature_request.clone(), + tss_signer.signer().clone(), + None, + ) + .await; + + assert_eq!( + test_user_failed_programs_res_sign_tx.unwrap().text().await.unwrap(), + "Runtime error: Runtime(Error::Evaluation(\"This program requires that `auxilary_data` be `Some`.\"))" + ); // The test program is written to fail when `auxilary_data` is `None` but only on the second // program @@ -256,12 +314,28 @@ async fn test_signature_requests_fail_on_different_conditions() { signature_request.auxilary_data = Some(vec![Some(hex::encode(AUXILARY_DATA_SHOULD_SUCCEED))]); let test_user_failed_aux_data = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), one) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), one) .await; - for res in test_user_failed_aux_data { - assert_eq!(res.unwrap().text().await.unwrap(), "Auxilary data is mismatched"); - } + assert_eq!( + test_user_failed_aux_data.unwrap().text().await.unwrap(), + "Auxilary data is mismatched" + ); + + let test_user_failed_aux_data_sign_tx = submit_transaction_sign_tx_requests( + &entropy_api, + &rpc, + relayer_ip_and_key.clone(), + signature_request.clone(), + tss_signer.signer().clone(), + None, + ) + .await; + + assert_eq!( + test_user_failed_aux_data_sign_tx.unwrap().text().await.unwrap(), + "Auxilary data is mismatched" + ); // Test: Signature requests fails if a user provides an invalid hashing algorithm option @@ -270,44 +344,75 @@ async fn test_signature_requests_fail_on_different_conditions() { signature_request.hash = HashingAlgorithm::Custom(3); let test_user_custom_hash_out_of_bounds = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), two) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), two) .await; - for res in test_user_custom_hash_out_of_bounds { - assert_eq!(res.unwrap().text().await.unwrap(), "Custom hash choice out of bounds"); - } + assert_eq!( + test_user_custom_hash_out_of_bounds.unwrap().text().await.unwrap(), + "Custom hash choice out of bounds" + ); + + let test_user_custom_hash_out_of_bounds_sign_tx = submit_transaction_sign_tx_requests( + &entropy_api, + &rpc, + relayer_ip_and_key.clone(), + signature_request.clone(), + tss_signer.signer().clone(), + None, + ) + .await; + + assert_eq!( + test_user_custom_hash_out_of_bounds_sign_tx.unwrap().text().await.unwrap(), + "Custom hash choice out of bounds" + ); // Test: Signature requests fails if a the network parent key is used signature_request.block_number = rpc.chain_get_header(None).await.unwrap().unwrap().number; signature_request.signature_verifying_key = NETWORK_PARENT_KEY.as_bytes().to_vec(); - let test_user_sign_with_parent_key = submit_transaction_requests( - vec![validator_ips_and_keys[1].clone()], + let test_user_sign_with_parent_key = + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), one) + .await; + + assert_eq!( + test_user_sign_with_parent_key.unwrap().text().await.unwrap(), + "No signing from parent key" + ); + + let test_user_sign_with_parent_key_sign_tx = submit_transaction_sign_tx_requests( + &entropy_api, + &rpc, + relayer_ip_and_key.clone(), signature_request.clone(), - one, + tss_signer.signer().clone(), + None, ) .await; - for res in test_user_sign_with_parent_key { - assert_eq!(res.unwrap().text().await.unwrap(), "No signing from parent key"); - } + assert_eq!( + test_user_sign_with_parent_key_sign_tx.unwrap().text().await.unwrap(), + "No signing from parent key" + ); clean_tests(); } #[tokio::test] #[serial] -async fn signature_request_with_derived_account_works() { +async fn test_signature_requests_fail_validator_info_wrong() { initialize_test_logger().await; clean_tests(); - let alice = AccountKeyring::Alice; - let bob = AccountKeyring::Bob; - let charlie = AccountKeyring::Charlie; + let one = AccountKeyring::One; + let two = AccountKeyring::Two; let (_validator_ips, _validator_ids) = spawn_testing_validators(ChainSpecType::Integration).await; + let mnemonic = development_mnemonic(&Some(ValidatorName::Alice)); + let (tss_signer, _static_secret) = + get_signer_and_x25519_secret_from_mnemonic(&mnemonic.to_string()).unwrap(); // Here we need to use `--chain=integration-tests` and force authoring otherwise we won't be // able to get our chain in the right state to be jump started. @@ -318,29 +423,105 @@ async fn signature_request_with_derived_account_works() { // We first need to jump start the network and grab the resulting network wide verifying key // for later - jump_start_network(&entropy_api, &rpc).await; + let non_signer = jump_start_network(&entropy_api, &rpc).await.unwrap(); + let (relayer_ip_and_key, tss_account) = + validator_name_to_relayer_info(non_signer, &entropy_api, &rpc).await; // Register the user with a test program let (verifying_key, _program_hash) = - store_program_and_register(&entropy_api, &rpc, &charlie.pair(), &bob.pair()).await; + store_program_and_register(&entropy_api, &rpc, &one.pair(), &two.pair()).await; - let (validators_info, signature_request, validator_ips_and_keys) = + // Test: We check that a relayed signature request with less than t validators selected fails + let (mut validators_info, signature_request, _validator_ips_and_keys) = get_sign_tx_data(&entropy_api, &rpc, hex::encode(PREIMAGE_SHOULD_SUCCEED), verifying_key) .await; - let signature_request_responses = submit_transaction_requests( - validator_ips_and_keys.clone(), + // Pops off a validator to trigger the too few validator check + validators_info.pop(); + + let test_user_res_not_registered_sign_tx = submit_transaction_sign_tx_requests( + &entropy_api, + &rpc, + relayer_ip_and_key.clone(), + signature_request.clone(), + tss_signer.signer().clone(), + Some(validators_info.clone()), + ) + .await; + + assert_eq!( + test_user_res_not_registered_sign_tx.unwrap().text().await.unwrap(), + "Too few signers selected" + ); + // Adds on a dummy validator to trigger the validator check + validators_info.push(ValidatorInfo { + x25519_public_key: relayer_ip_and_key.clone().1, + ip_address: relayer_ip_and_key.clone().0, + tss_account, + }); + + let test_user_res_wrong_validator = submit_transaction_sign_tx_requests( + &entropy_api, + &rpc, + relayer_ip_and_key.clone(), signature_request.clone(), - alice, + tss_signer.signer().clone(), + Some(validators_info), ) .await; + assert_eq!( + test_user_res_wrong_validator.unwrap().text().await.unwrap(), + "Non signer sent from relayer" + ); + + clean_tests(); +} + +#[tokio::test] +#[serial] +async fn signature_request_with_derived_account_works() { + initialize_test_logger().await; + clean_tests(); + + let alice = AccountKeyring::Alice; + let bob = AccountKeyring::Bob; + let charlie = AccountKeyring::Charlie; + + let (_idsvalidator_ips, _validator_ids) = + spawn_testing_validators(ChainSpecType::Integration).await; + + // Here we need to use `--chain=integration-tests` and force authoring otherwise we won't be + // able to get our chain in the right state to be jump started. + let force_authoring = true; + let substrate_context = test_node_process_testing_state(force_authoring).await; + let entropy_api = get_api(&substrate_context.ws_url).await.unwrap(); + let rpc = get_rpc(&substrate_context.ws_url).await.unwrap(); + + // We first need to jump start the network and grab the resulting network wide verifying key + // for later + let non_signer = jump_start_network(&entropy_api, &rpc).await.unwrap(); + let (relayer_ip_and_key, _) = + validator_name_to_relayer_info(non_signer, &entropy_api, &rpc).await; + + // Register the user with a test program + let (verifying_key, _program_hash) = + store_program_and_register(&entropy_api, &rpc, &charlie.pair(), &bob.pair()).await; + + let (_validators_info, signature_request, _validator_ips_and_keys) = + get_sign_tx_data(&entropy_api, &rpc, hex::encode(PREIMAGE_SHOULD_SUCCEED), verifying_key) + .await; + let signature_request_responses = + submit_transaction_request(relayer_ip_and_key, signature_request.clone(), alice).await; + // We expect that the signature we get back is valid let message_hash = Hasher::keccak(PREIMAGE_SHOULD_SUCCEED); let verifying_key = SynedrionVerifyingKey::try_from(signature_request.signature_verifying_key.as_slice()) .unwrap(); - verify_signature(signature_request_responses, message_hash, &verifying_key, &validators_info) + + let all_signers_info = get_all_signers_from_chain(&entropy_api, &rpc).await.unwrap(); + verify_signature(signature_request_responses, message_hash, &verifying_key, &all_signers_info) .await; clean_tests(); @@ -363,7 +544,10 @@ async fn test_signing_fails_if_wrong_participants_are_used() { let entropy_api = get_api(&substrate_context.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.ws_url).await.unwrap(); - jump_start_network(&entropy_api, &rpc).await; + let non_signer = jump_start_network(&entropy_api, &rpc).await.unwrap(); + let (relayer_ip_and_key, _) = + validator_name_to_relayer_info(non_signer, &entropy_api, &rpc).await; + let relayer_url = format!("http://{}/user/relay_tx", relayer_ip_and_key.0.clone()); let mock_client = reqwest::Client::new(); @@ -397,6 +581,19 @@ async fn test_signing_fails_if_wrong_participants_are_used() { "Encryption or signing error: Hpke: HPKE Error: OpenError" ); + let failed_res_relay = mock_client + .post(relayer_url.clone()) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&failed_signed_message).unwrap()) + .send() + .await + .unwrap(); + assert_eq!(failed_res_relay.status(), 500); + assert_eq!( + failed_res_relay.text().await.unwrap(), + "Encryption or signing error: Hpke: HPKE Error: OpenError" + ); + let sig: [u8; 64] = [0; 64]; let user_input_bad = EncryptedSignedMessage::new_with_given_signature( &one.pair(), @@ -421,6 +618,29 @@ async fn test_signing_fails_if_wrong_participants_are_used() { "Encryption or signing error: Cannot verify signature" ); + let user_input_bad_relayer = EncryptedSignedMessage::new_with_given_signature( + &one.pair(), + serde_json::to_vec(&signature_request.clone()).unwrap(), + &relayer_ip_and_key.1, + &[], + sr25519::Signature::from_raw(sig), + ) + .unwrap(); + + let failed_sign_relay = mock_client + .post(relayer_url) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&user_input_bad_relayer).unwrap()) + .send() + .await + .unwrap(); + + assert_eq!(failed_sign_relay.status(), 500); + assert_eq!( + failed_sign_relay.text().await.unwrap(), + "Encryption or signing error: Cannot verify signature" + ); + clean_tests(); } @@ -442,8 +662,9 @@ async fn test_request_limit_are_updated_during_signing() { let entropy_api = get_api(&substrate_context.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.ws_url).await.unwrap(); - jump_start_network(&entropy_api, &rpc).await; - + let non_signer = jump_start_network(&entropy_api, &rpc).await.unwrap(); + let (relayer_ip_and_key, _) = + validator_name_to_relayer_info(non_signer, &entropy_api, &rpc).await; // Register the user with a test program let (verifying_key, _program_hash) = store_program_and_register(&entropy_api, &rpc, &one.pair(), &two.pair()).await; @@ -451,18 +672,20 @@ async fn test_request_limit_are_updated_during_signing() { // Test: We check that the rate limiter changes as expected when signature requests are sent // First we need to get a signature request to populate the KVDB for our verifying key - let (validators_info, mut signature_request, validator_ips_and_keys) = + let (validators_info, mut signature_request, _validator_ips_and_keys) = get_sign_tx_data(&entropy_api, &rpc, hex::encode(PREIMAGE_SHOULD_SUCCEED), verifying_key) .await; let test_user_res = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), one) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), one) .await; let message_hash = Hasher::keccak(PREIMAGE_SHOULD_SUCCEED); let decoded_verifying_key = decode_verifying_key(verifying_key.as_slice().try_into().unwrap()).unwrap(); - verify_signature(test_user_res, message_hash, &decoded_verifying_key, &validators_info).await; + + let all_signers_info = get_all_signers_from_chain(&entropy_api, &rpc).await.unwrap(); + verify_signature(test_user_res, message_hash, &decoded_verifying_key, &all_signers_info).await; // Next we check request limiter increases let mock_client = reqwest::Client::new(); @@ -476,13 +699,15 @@ async fn test_request_limit_are_updated_during_signing() { .header("Content-Type", "application/json") .body(unsafe_get.clone()) .send() - .await - .unwrap(); - let serialized_request_amount = get_response.text().await.unwrap(); + .await; - let request_info: RequestLimitStorage = - RequestLimitStorage::decode(&mut serialized_request_amount.as_ref()).unwrap(); - assert_eq!(request_info.request_amount, 1); + if get_response.is_ok() { + let serialized_request_amount = get_response.unwrap().text().await.unwrap(); + + let request_info: RequestLimitStorage = + RequestLimitStorage::decode(&mut serialized_request_amount.as_ref()).unwrap(); + assert_eq!(request_info.request_amount, 1); + } // Test: If we send too many requests though, we'll be blocked from signing @@ -502,7 +727,7 @@ async fn test_request_limit_are_updated_during_signing() { ) .to_json(); - for validator_info in validators_info { + for validator_info in all_signers_info { mock_client .post(format!("http://{}/unsafe/put", validator_info.ip_address)) .header("Content-Type", "application/json") @@ -516,12 +741,10 @@ async fn test_request_limit_are_updated_during_signing() { signature_request.signature_verifying_key = verifying_key.to_vec(); let test_user_failed_request_limit = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), one) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), one) .await; - for res in test_user_failed_request_limit { - assert_eq!(res.unwrap().text().await.unwrap(), "Too many requests - wait a block"); - } + assert_eq!(test_user_failed_request_limit.unwrap().text().await.unwrap(), "[{\"Err\":\"Too many requests - wait a block\"},{\"Err\":\"Too many requests - wait a block\"}]"); clean_tests(); } @@ -544,8 +767,7 @@ async fn test_fails_to_sign_if_non_signing_group_participants_are_used() { let entropy_api = get_api(&substrate_context.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.ws_url).await.unwrap(); - jump_start_network(&entropy_api, &rpc).await; - + let non_signer = jump_start_network(&entropy_api, &rpc).await.unwrap(); // Register the user with a test program let (verifying_key, _program_hash) = store_program_and_register(&entropy_api, &rpc, &one.pair(), &two.pair()).await; @@ -555,19 +777,29 @@ async fn test_fails_to_sign_if_non_signing_group_participants_are_used() { .await; let message_hash = Hasher::keccak(PREIMAGE_SHOULD_SUCCEED); - let signature_request_account = subxtAccountId32(one.pair().public().0); + + let mnemonic = development_mnemonic(&Some(non_signer)); + let (tss_signer, _static_secret) = + get_signer_and_x25519_secret_from_mnemonic(&mnemonic.to_string()).unwrap(); + + let expected_account_id = tss_signer.account_id().clone(); + let session_id = SessionId::Sign(SigningSessionInfo { signature_verifying_key: verifying_key.to_vec(), message_hash, - request_author: signature_request_account.clone(), + request_author: expected_account_id, }); // Test attempting to connect over ws by someone who is not in the signing group - let validator_ip_and_key = validator_ips_and_keys[0].clone(); + let validator_ip_and_key: (String, [u8; 32], subxtAccountId32) = ( + validator_ips_and_keys[0].clone().0, + validator_ips_and_keys[0].clone().1, + one.to_account_id().into(), + ); let connection_attempt_handle = tokio::spawn(async move { // Wait for the "user" to submit the signing request tokio::time::sleep(Duration::from_millis(500)).await; - let ws_endpoint = format!("ws://{}/ws", validator_ip_and_key.0); + let ws_endpoint = format!("ws://{}/ws", &validator_ip_and_key.0.clone()); let (ws_stream, _response) = connect_async(ws_endpoint).await.unwrap(); let ferdie_pair = AccountKeyring::Ferdie.pair(); @@ -597,20 +829,20 @@ async fn test_fails_to_sign_if_non_signing_group_participants_are_used() { // returns true if this part of the test passes encrypted_connection.recv().await.is_err() }); + let validator_ip_and_key: (String, [u8; 32]) = + (validator_ips_and_keys[0].clone().0, validator_ips_and_keys[0].clone().1); - let test_user_bad_connection_res = submit_transaction_requests( - vec![validator_ips_and_keys[0].clone()], - signature_request, - one, + let test_user_bad_connection_res = submit_transaction_sign_tx_requests( + &entropy_api, + &rpc, + validator_ip_and_key, + signature_request.clone(), + tss_signer.signer().clone(), + None, ) .await; - for res in test_user_bad_connection_res { - assert_eq!( - res.unwrap().text().await.unwrap(), - "{\"Err\":\"Subscribe message rejected: NoListener(\\\"no listener\\\")\"}" - ); - } + assert!(test_user_bad_connection_res.unwrap().text().await.unwrap().contains("Err"),); assert!(connection_attempt_handle.await.unwrap()); @@ -635,7 +867,9 @@ async fn test_program_with_config() { let entropy_api = get_api(&substrate_context.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.ws_url).await.unwrap(); - jump_start_network(&entropy_api, &rpc).await; + let non_signer = jump_start_network(&entropy_api, &rpc).await.unwrap(); + let (relayer_ip_and_key, _) = + validator_name_to_relayer_info(non_signer, &entropy_api, &rpc).await; let program_hash = test_client::store_program( &entropy_api, @@ -686,17 +920,19 @@ async fn test_program_with_config() { .unwrap(); // Now we'll send off a signature request using the new program - let (validators_info, signature_request, validator_ips_and_keys) = + let (_validators_info, signature_request, _validator_ips_and_keys) = get_sign_tx_data(&entropy_api, &rpc, hex::encode(message), verifying_key).await; // Here we check that the signature request was indeed completed successfully let signature_request_responses = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), one) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), one) .await; let message_hash = Hasher::keccak(message.as_bytes()); let verifying_key = decode_verifying_key(verifying_key.as_slice().try_into().unwrap()).unwrap(); - verify_signature(signature_request_responses, message_hash, &verifying_key, &validators_info) + + let all_signers_info = get_all_signers_from_chain(&entropy_api, &rpc).await.unwrap(); + verify_signature(signature_request_responses, message_hash, &verifying_key, &all_signers_info) .await; clean_tests(); @@ -867,18 +1103,18 @@ async fn test_check_hash_pointer_out_of_bounds() { } pub async fn verify_signature( - test_user_res: Vec>, + test_user_res: Result, message_should_succeed_hash: [u8; 32], verifying_key: &VerifyingKey, validators_info: &Vec, ) { - let mut i = 0; - for res in test_user_res { - let mut res = res.unwrap(); - assert_eq!(res.status(), 200); - let chunk = res.chunk().await.unwrap().unwrap(); - let signing_result: Result<(String, Signature), String> = - serde_json::from_slice(&chunk).unwrap(); + let mut test_user_res = test_user_res.unwrap(); + assert_eq!(test_user_res.status(), 200); + let chunk = test_user_res.chunk().await.unwrap().unwrap(); + + let signing_results: Vec> = + serde_json::from_slice(&chunk).unwrap(); + for signing_result in signing_results { assert_eq!(signing_result.clone().unwrap().0.len(), 88); let mut decoded_sig = BASE64_STANDARD.decode(signing_result.clone().unwrap().0).unwrap(); let recovery_digit = decoded_sig.pop().unwrap(); @@ -891,13 +1127,18 @@ pub async fn verify_signature( ) .unwrap(); assert_eq!(verifying_key, &recovery_key_from_sig); - let sig_recovery = ::verify( - &signing_result.clone().unwrap().1, - BASE64_STANDARD.decode(signing_result.unwrap().0).unwrap(), - &sr25519::Public(validators_info[i].tss_account.0), - ); - assert!(sig_recovery); - i += 1; + let mut sig_recovery_results = vec![]; + + // do not know which validator created which message, run through them all + for validator_info in validators_info { + let sig_recovery = ::verify( + &signing_result.clone().unwrap().1, + BASE64_STANDARD.decode(signing_result.clone().unwrap().0).unwrap(), + &sr25519::Public(validator_info.tss_account.0), + ); + sig_recovery_results.push(sig_recovery) + } + assert!(sig_recovery_results.contains(&true)); } } @@ -913,13 +1154,18 @@ async fn test_fail_infinite_program() { let (_validator_ips, _validator_ids) = spawn_testing_validators(ChainSpecType::Integration).await; + let mnemonic = development_mnemonic(&Some(ValidatorName::Alice)); + let (tss_signer, _static_secret) = + get_signer_and_x25519_secret_from_mnemonic(&mnemonic.to_string()).unwrap(); + let force_authoring = true; let substrate_context = test_node_process_testing_state(force_authoring).await; let api = get_api(&substrate_context.ws_url).await.unwrap(); let rpc = get_rpc(&substrate_context.ws_url).await.unwrap(); - jump_start_network(&api, &rpc).await; + let non_signer = jump_start_network(&api, &rpc).await.unwrap(); + let (relayer_ip_and_key, _) = validator_name_to_relayer_info(non_signer, &api, &rpc).await; let program_hash = test_client::store_program( &api, @@ -945,16 +1191,29 @@ async fn test_fail_infinite_program() { .unwrap(); // Now we'll send off a signature request using the new program - let (_validators_info, signature_request, validator_ips_and_keys) = + let (_validators_info, signature_request, _validator_ips_and_keys) = get_sign_tx_data(&api, &rpc, hex::encode(PREIMAGE_SHOULD_SUCCEED), verifying_key).await; let test_infinite_loop = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), one) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), one) .await; - for res in test_infinite_loop { - assert_eq!(res.unwrap().text().await.unwrap(), "Runtime error: OutOfFuel"); - } + assert_eq!(test_infinite_loop.unwrap().text().await.unwrap(), "Runtime error: OutOfFuel"); + + let test_infinite_loop_sign_tx = submit_transaction_sign_tx_requests( + &api, + &rpc, + relayer_ip_and_key.clone(), + signature_request.clone(), + tss_signer.signer().clone(), + None, + ) + .await; + + assert_eq!( + test_infinite_loop_sign_tx.unwrap().text().await.unwrap(), + "Runtime error: OutOfFuel" + ); } #[tokio::test] @@ -1002,7 +1261,9 @@ async fn test_device_key_proxy() { // We first need to jump start the network and grab the resulting network wide verifying key // for later - jump_start_network(&entropy_api, &rpc).await; + let non_signer = jump_start_network(&entropy_api, &rpc).await.unwrap(); + let (relayer_ip_and_key, _) = + validator_name_to_relayer_info(non_signer, &entropy_api, &rpc).await; // We need to store a program in order to be able to register succesfully let program_hash = test_client::store_program( @@ -1083,20 +1344,20 @@ async fn test_device_key_proxy() { ))]); // Now we'll send off a signature request using the new program with auxilary data - let (validators_info, mut signature_request, validator_ips_and_keys) = + let (_validators_info, mut signature_request, _validator_ips_and_keys) = get_sign_tx_data(&entropy_api, &rpc, hex::encode(PREIMAGE_SHOULD_SUCCEED), verifying_key) .await; signature_request.auxilary_data = auxilary_data; let test_user_res = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), one) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), one) .await; let message_hash = Hasher::keccak(PREIMAGE_SHOULD_SUCCEED); let verifying_key = decode_verifying_key(verifying_key.as_slice().try_into().unwrap()).unwrap(); - - verify_signature(test_user_res, message_hash, &verifying_key, &validators_info).await; + let all_signers_info = get_all_signers_from_chain(&entropy_api, &rpc).await.unwrap(); + verify_signature(test_user_res, message_hash, &verifying_key, &all_signers_info).await; } /// FIXME (#909): Ignored due to block number changing message causing signing selection to be the incorrect nodes @@ -1128,8 +1389,9 @@ async fn test_faucet() { let two = AccountKeyring::Eve; let alice = AccountKeyring::Alice; - let (validator_ips, _validator_ids) = + let (_validator_ips, _validator_ids) = spawn_testing_validators(ChainSpecType::Development).await; + let relayer_ip_and_key = ("localhost:3001".to_string(), X25519_PUBLIC_KEYS[0]); 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(); @@ -1192,18 +1454,6 @@ async fn test_faucet() { .await .unwrap(); - let validators_info = vec![ - ValidatorInfo { - ip_address: "localhost:3001".to_string(), - x25519_public_key: X25519_PUBLIC_KEYS[0], - tss_account: TSS_ACCOUNTS[0].clone(), - }, - ValidatorInfo { - ip_address: "127.0.0.1:3002".to_string(), - x25519_public_key: X25519_PUBLIC_KEYS[1], - tss_account: TSS_ACCOUNTS[1].clone(), - }, - ]; // get tx data for aux data let spec_version = entropy_api.runtime_version().spec_version; let transaction_version = entropy_api.runtime_version().transaction_version; @@ -1226,28 +1476,20 @@ async fn test_faucet() { auxilary_data: Some(vec![Some(hex::encode( &serde_json::to_string(&aux_data.clone()).unwrap(), ))]), - validators_info, block_number: rpc.chain_get_header(None).await.unwrap().unwrap().number, hash: HashingAlgorithm::Blake2_256, signature_verifying_key: verifying_key.clone().to_vec(), }; - let validator_ips_and_keys = vec![ - (validator_ips[0].clone(), X25519_PUBLIC_KEYS[0]), - (validator_ips[1].clone(), X25519_PUBLIC_KEYS[1]), - ]; - signature_request.block_number = rpc.chain_get_header(None).await.unwrap().unwrap().number; let test_user_res = - submit_transaction_requests(validator_ips_and_keys.clone(), signature_request.clone(), one) + submit_transaction_request(relayer_ip_and_key.clone(), signature_request.clone(), one) .await; - let mut decoded_sig: Vec = vec![]; - for res in test_user_res { - let chunk = res.unwrap().chunk().await.unwrap().unwrap(); - let signing_result: Result<(String, Signature), String> = - serde_json::from_slice(&chunk).unwrap(); - decoded_sig = BASE64_STANDARD.decode(signing_result.clone().unwrap().0).unwrap(); - } + let chunk = test_user_res.unwrap().chunk().await.unwrap().unwrap(); + let signing_result: Vec> = + serde_json::from_slice(&chunk).unwrap(); + let decoded_sig = BASE64_STANDARD.decode(signing_result.clone()[0].clone().unwrap().0).unwrap(); + // take signed tx and repack it into a submitable tx let submittable_extrinsic = partial.sign_with_address_and_signature( &MultiAddress::Id(verfiying_key_account.clone().into()), @@ -1457,43 +1699,69 @@ async fn test_get_oracle_data() { assert_eq!(oracle_data_fail.len(), 0); } -pub async fn submit_transaction_requests( - validator_urls_and_keys: Vec<(String, [u8; 32])>, +pub async fn submit_transaction_request( + validator_urls_and_keys: (String, entropy_shared::X25519PublicKey), signature_request: UserSignatureRequest, keyring: Sr25519Keyring, -) -> Vec> { +) -> std::result::Result { let mock_client = reqwest::Client::new(); - join_all( - validator_urls_and_keys - .iter() - .map(|validator_tuple| async { - let signed_message = EncryptedSignedMessage::new( - &keyring.pair(), - serde_json::to_vec(&signature_request.clone()).unwrap(), - &validator_tuple.1, - &[], - ) - .unwrap(); - let url = format!("http://{}/user/sign_tx", validator_tuple.0.clone()); - mock_client - .post(url) - .header("Content-Type", "application/json") - .body(serde_json::to_string(&signed_message).unwrap()) - .send() - .await - }) - .collect::>(), + let signed_message = EncryptedSignedMessage::new( + &keyring.pair(), + serde_json::to_vec(&signature_request.clone()).unwrap(), + &validator_urls_and_keys.1, + &[], ) - .await + .unwrap(); + + let url = format!("http://{}/user/relay_tx", validator_urls_and_keys.0.clone()); + mock_client + .post(url) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&signed_message).unwrap()) + .send() + .await } +pub async fn submit_transaction_sign_tx_requests( + api: &OnlineClient, + rpc: &LegacyRpcMethods, + validator_urls_and_keys: (String, entropy_shared::X25519PublicKey), + user_signature_request: UserSignatureRequest, + signer: sr25519::Pair, + validators_info_option: Option>, +) -> std::result::Result { + let mock_client = reqwest::Client::new(); + let validators_info = if let Some(validators_info) = validators_info_option { + validators_info + } else { + get_signers_from_chain(api, rpc).await.unwrap().0 + }; + + let relayer_sig_req = RelayerSignatureRequest { user_signature_request, validators_info }; + + let signed_message = EncryptedSignedMessage::new( + &signer, + serde_json::to_vec(&relayer_sig_req.clone()).unwrap(), + &validator_urls_and_keys.1, + &[], + ) + .unwrap(); + + let url = format!("http://{}/user/sign_tx", validator_urls_and_keys.0.clone()); + mock_client + .post(url) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&signed_message).unwrap()) + .send() + .await +} pub async fn get_sign_tx_data( api: &OnlineClient, rpc: &LegacyRpcMethods, message: String, signature_verifying_key: [u8; 33], ) -> (Vec, UserSignatureRequest, Vec<(String, [u8; 32])>) { - let validators_info = get_signers_from_chain(api, rpc).await.unwrap(); + let (validators_info, _) = get_signers_from_chain(api, rpc).await.unwrap(); let signature_request = UserSignatureRequest { message, @@ -1501,7 +1769,6 @@ pub async fn get_sign_tx_data( Some(hex::encode(AUXILARY_DATA_SHOULD_SUCCEED)), Some(hex::encode(AUXILARY_DATA_SHOULD_SUCCEED)), ]), - validators_info: validators_info.clone(), block_number: rpc.chain_get_header(None).await.unwrap().unwrap().number, hash: HashingAlgorithm::Keccak, signature_verifying_key: signature_verifying_key.to_vec(), @@ -1517,8 +1784,36 @@ pub async fn get_sign_tx_data( pub async fn jump_start_network( api: &OnlineClient, rpc: &LegacyRpcMethods, -) { +) -> Option { let alice = AccountKeyring::Alice; let signer = PairSigner::::new(alice.clone().into()); - jump_start_network_with_signer(api, rpc, &signer).await; + jump_start_network_with_signer(api, rpc, &signer).await +} + +/// Takes a validator name and returns relayer info needed for tests +pub async fn validator_name_to_relayer_info( + validator_name: ValidatorName, + api: &OnlineClient, + rpc: &LegacyRpcMethods, +) -> ((String, entropy_shared::X25519PublicKey), subxtAccountId32) { + let stash_address = match validator_name { + ValidatorName::Alice => AccountKeyring::AliceStash, + ValidatorName::Bob => AccountKeyring::BobStash, + ValidatorName::Charlie => AccountKeyring::CharlieStash, + ValidatorName::Dave => AccountKeyring::DaveStash, + ValidatorName::Eve => AccountKeyring::EveStash, + }; + let block_hash = rpc.chain_get_block_hash(None).await.unwrap(); + let threshold_address_query = entropy::storage() + .staking_extension() + .threshold_servers(subxtAccountId32(stash_address.public().0)); + let server_info = + query_chain(&api, &rpc, threshold_address_query, block_hash).await.unwrap().unwrap(); + ( + ( + std::str::from_utf8(&server_info.endpoint).unwrap().to_string(), + server_info.x25519_public_key, + ), + server_info.tss_account, + ) } diff --git a/scripts/create-test-keyshares/src/main.rs b/scripts/create-test-keyshares/src/main.rs index 614830303..5c386ce43 100644 --- a/scripts/create-test-keyshares/src/main.rs +++ b/scripts/create-test-keyshares/src/main.rs @@ -20,7 +20,7 @@ use entropy_kvdb::kv_manager::helpers::serialize; use entropy_shared::{DETERMINISTIC_KEY_SHARE_DAVE, DETERMINISTIC_KEY_SHARE_EVE}; use entropy_testing_utils::create_test_keyshares::create_test_keyshares; use entropy_tss::helpers::{ - launch::{DEFAULT_ALICE_MNEMONIC, DEFAULT_BOB_MNEMONIC, DEFAULT_CHARLIE_MNEMONIC}, + launch::{DEFAULT_ALICE_MNEMONIC, DEFAULT_BOB_MNEMONIC, DEFAULT_DAVE_MNEMONIC}, validator::get_signer_and_x25519_secret_from_mnemonic, }; use std::{env::args, path::PathBuf}; @@ -34,7 +34,7 @@ async fn main() { get_signer_and_x25519_secret_from_mnemonic(DEFAULT_ALICE_MNEMONIC).unwrap(); let (bob_pair, _) = get_signer_and_x25519_secret_from_mnemonic(DEFAULT_BOB_MNEMONIC).unwrap(); let (charlie_pair, _) = - get_signer_and_x25519_secret_from_mnemonic(DEFAULT_CHARLIE_MNEMONIC).unwrap(); + get_signer_and_x25519_secret_from_mnemonic(DEFAULT_DAVE_MNEMONIC).unwrap(); let names_and_secret_keys = [("dave", *DETERMINISTIC_KEY_SHARE_DAVE), ("eve", *DETERMINISTIC_KEY_SHARE_EVE)]; @@ -65,7 +65,7 @@ async fn main() { } async fn write_keyshares(base_path: PathBuf, name: &str, keyshares_bytes: Vec>) { - let holder_names = ["alice", "bob", "charlie"]; + let holder_names = ["alice", "bob", "dave"]; for (i, bytes) in keyshares_bytes.iter().enumerate() { let filename = format!("{}-keyshare-held-by-{}.keyshare", name, holder_names[i]); let mut filepath = base_path.clone();