Skip to content

Commit

Permalink
Add /relay_tx endpoint (#1050)
Browse files Browse the repository at this point in the history
* add relay tx endpoint

* working

* redesign test

* fix sig recovery check

* add pre sign checks

* add signer check

* add validator checks

* remove validator info from user sig req message

* fixing tests

* another test

* fix user tests

* more tests

* clean

* test

* clean

* fmt

* fmt

* docs

* tests

* fmt

* fix tests

* fix test

* Apply suggestions from code review

Co-authored-by: peg <peg@magmacollective.org>
Co-authored-by: David <dvdplm@gmail.com>

* Update crates/threshold-signature-server/src/user/api.rs

Co-authored-by: peg <peg@magmacollective.org>

* move get_signers_from_chain

* return option validator name

* random selection

* add validator checks

* clean tests

* tests

* remove poping selected signers

* fix

* Apply suggestions from code review

Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>
Co-authored-by: peg <peg@magmacollective.org>

* collapse user_sig_request into relayer request

* Apply suggestions from code review

Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>
Co-authored-by: peg <peg@magmacollective.org>

* clean

* clean

* clean

* clean

* Update crates/threshold-signature-server/src/user/tests.rs

Co-authored-by: peg <peg@magmacollective.org>

* clean

* replace charlie with dave in key creation

* fmt

* lint

* test fix

---------

Co-authored-by: peg <peg@magmacollective.org>
Co-authored-by: David <dvdplm@gmail.com>
Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>
  • Loading branch information
4 people authored Sep 26, 2024
1 parent 2800d61 commit 9facf6e
Show file tree
Hide file tree
Showing 27 changed files with 1,000 additions and 323 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Responses>`
from the signers.

### Added
- Jumpstart network ([#918](https://github.com/entropyxyz/entropy-core/pull/918))
Expand All @@ -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))
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
123 changes: 59 additions & 64 deletions crates/client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
};
Expand Down Expand Up @@ -113,14 +115,13 @@ pub async fn sign(
) -> Result<RecoverableSignature, ClientError> {
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(),
Expand All @@ -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::<Vec<_>>();

// 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<Result<(String, Signature), String>> = 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 = <sr25519::Pair as Pair>::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
Expand Down
73 changes: 62 additions & 11 deletions crates/client/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<Option<String>>>,
/// Information from the validators in signing party
pub validators_info: Vec<ValidatorInfo>,
/// When the message was created and signed
pub block_number: BlockNumber,
/// Hashing algorithm to be used for signing
Expand All @@ -40,24 +38,32 @@ pub struct UserSignatureRequest {
pub signature_verifying_key: Vec<u8>,
}

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<ValidatorInfo>,
}

/// 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<EntropyConfig>,
rpc: &LegacyRpcMethods<EntropyConfig>,
) -> Result<Vec<ValidatorInfo>, 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();

Expand Down Expand Up @@ -85,6 +91,51 @@ pub async fn get_signers_from_chain(
handles.push(handle);
}

let mut all_validators: Vec<ValidatorInfo> = 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<EntropyConfig>,
rpc: &LegacyRpcMethods<EntropyConfig>,
) -> Result<Vec<ValidatorInfo>, 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<Result<ValidatorInfo, SubgroupGetError>> =
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<ValidatorInfo> = vec![];
for handle in handles {
all_signers.push(handle.await??);
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
7 changes: 4 additions & 3 deletions crates/testing-utils/src/create_test_keyshares.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<BTreeSet<_>>();

let old_holders = all_parties.clone().into_iter().take(2).collect::<BTreeSet<_>>();
let mut old_holders = all_parties.clone();
old_holders.remove(&PartyId::from(charlie.clone().public()));

let keyshares =
KeyShare::<Params, PartyId>::new_centralized(&mut OsRng, &old_holders, Some(&signing_key));
let aux_infos = AuxInfo::<Params, PartyId>::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,
Expand Down
10 changes: 5 additions & 5 deletions crates/threshold-signature-server/src/helpers/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,7 +41,7 @@ use crate::{
#[tracing::instrument(skip(app_state), level = tracing::Level::DEBUG)]
pub async fn do_signing(
rpc: &LegacyRpcMethods<EntropyConfig>,
user_signature_request: UserSignatureRequest,
relayer_signature_request: RelayerSignatureRequest,
app_state: &AppState,
signing_session_info: SigningSessionInfo,
request_limit: u32,
Expand All @@ -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
Expand All @@ -64,15 +64,15 @@ 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<AccountId32> = user_signature_request
let tss_accounts: Vec<AccountId32> = relayer_signature_request
.validators_info
.iter()
.map(|validator_info| validator_info.tss_account.clone())
.collect();

// 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());

Expand Down
Loading

0 comments on commit 9facf6e

Please sign in to comment.