Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non persistent TSS signer and x25519 keypair #1216

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1de907a
Dont allow mnemonic to be passed in via CLI, or environment variable …
ameba23 Oct 22, 2024
6b97f8f
Changelog
ameba23 Oct 22, 2024
7bd4358
Error handling
ameba23 Oct 22, 2024
4fcfc30
Add endpoint giving public keys
ameba23 Oct 23, 2024
e1649a4
Document new endpoint
ameba23 Oct 23, 2024
b12149b
Changelog
ameba23 Oct 23, 2024
53d5175
Clippy
ameba23 Oct 23, 2024
7c7acc7
Merge master
ameba23 Nov 8, 2024
a2c9cd7
Merge master
ameba23 Nov 18, 2024
5b8db51
Merge master
ameba23 Dec 5, 2024
1de6e81
Fix lockfile
ameba23 Dec 5, 2024
e97aa16
Merge branch 'master' into peg/generate-mnemonic
ameba23 Dec 13, 2024
8a8cb52
Add keys to appstate
ameba23 Dec 13, 2024
542849e
Rm persisted TSS keys
ameba23 Dec 13, 2024
15d3bbe
Tidy following app state change
ameba23 Dec 13, 2024
1ca0a0c
Fixes for tests and test helpers
ameba23 Dec 16, 2024
1303be2
Revert commented out import
ameba23 Dec 16, 2024
ebc339e
Clippy
ameba23 Dec 16, 2024
87c6afd
Update unsafe get test
ameba23 Dec 16, 2024
e71cc72
Rm setup only option, tidy
ameba23 Dec 16, 2024
6831f49
Tidy AppState interface
ameba23 Dec 16, 2024
1c36ae0
Allow for entropy-tss to be put in a non-ready state
ameba23 Dec 16, 2024
e416800
Update node info test
ameba23 Dec 16, 2024
7d8690b
Make app state ready in tests
ameba23 Dec 17, 2024
96c6a1a
Comments
ameba23 Dec 17, 2024
91ac834
Fix node info test
ameba23 Dec 17, 2024
b59d78d
Update pre-requisite checks
ameba23 Dec 17, 2024
c7d5ca2
Clippy
ameba23 Dec 17, 2024
a05a2a3
Force getting minimum balance before start
ameba23 Dec 17, 2024
079c394
Clippy
ameba23 Dec 17, 2024
12e940d
Comments
ameba23 Dec 17, 2024
a71521b
Fixes, add helper
ameba23 Dec 17, 2024
9d2f80d
Merge master
ameba23 Dec 18, 2024
7cb7ac6
Changelog
ameba23 Dec 18, 2024
3c08c75
Improve display of failed balance check errors
ameba23 Dec 18, 2024
c18124e
Improve display of failed registration checks
ameba23 Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ runtime
- In [#1147](https://github.com/entropyxyz/entropy-core/pull/1147) a field is added to the
chainspec: `jump_started_signers` which allows the chain to be started in a pre-jumpstarted state
for testing. If this is not desired it should be set to `None`.
- In [#1216](https://github.com/entropyxyz/entropy-core/pull/1216) the `--setup-only` option for `entropy-tss`
was removed. `entropy-tss` should be started only once, and the public keys retrieved using the `/info`
http route.

### Added
- In [#1128](https://github.com/entropyxyz/entropy-core/pull/1128) an `/info` route was added to `entropy-tss`
Expand All @@ -52,6 +55,7 @@ runtime
- Update programs to accept multiple oracle data ([#1153](https://github.com/entropyxyz/entropy-core/pull/1153/))
- Use context, not block number in TDX quote input data ([#1179](https://github.com/entropyxyz/entropy-core/pull/1179))
- Allow offchain worker requests to all TSS nodes in entropy-tss test environment ([#1147](https://github.com/entropyxyz/entropy-core/pull/1147))
- Non persistent TSS signer and x25519 keypair ([#1216](https://github.com/entropyxyz/entropy-core/pull/1216))

### Fixed

Expand Down
2 changes: 2 additions & 0 deletions crates/protocol/src/protocol_transport/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub enum WsError {
Serialization(#[from] bincode::Error),
#[error("Received bad subscribe message")]
BadSubscribeMessage,
#[error("Node has started fresh and is not yet successfully set up")]
NotReady,
}

/// An error relating to handling a `ProtocolMessage`
Expand Down
15 changes: 6 additions & 9 deletions crates/threshold-signature-server/src/attestation/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use crate::{
attestation::errors::AttestationErr,
chain_api::{entropy, get_api, get_rpc, EntropyConfig},
get_signer_and_x25519_secret,
helpers::{
launch::LATEST_BLOCK_NUMBER_ATTEST,
substrate::{query_chain, submit_transaction},
Expand Down Expand Up @@ -46,7 +45,6 @@ pub async fn attest(
State(app_state): State<AppState>,
input: Bytes,
) -> Result<StatusCode, AttestationErr> {
let (signer, x25519_secret) = get_signer_and_x25519_secret(&app_state.kv_store).await?;
let attestation_requests = OcwMessageAttestationRequest::decode(&mut input.as_ref())?;

let api = get_api(&app_state.configuration.endpoint).await?;
Expand All @@ -59,15 +57,15 @@ pub async fn attest(
validate_new_attestation(block_number, &attestation_requests, &app_state.kv_store).await?;

// Check whether there is an attestion request for us
if !attestation_requests.tss_account_ids.contains(&signer.signer().public().0) {
if !attestation_requests.tss_account_ids.contains(&app_state.pair.public().0) {
return Ok(StatusCode::OK);
}

// Get the input nonce for this attestation
// Also acts as chain check to make sure data is on chain
let nonce = {
let pending_attestation_query =
entropy::storage().attestation().pending_attestations(signer.account_id());
entropy::storage().attestation().pending_attestations(app_state.signer().account_id());
query_chain(&api, &rpc, pending_attestation_query, None)
.await?
.ok_or_else(|| AttestationErr::Unexpected)?
Expand All @@ -76,11 +74,11 @@ pub async fn attest(
// TODO (#1181): since this endpoint is currently only used in tests we don't know what the context should be
let context = QuoteContext::Validate;

let quote = create_quote(nonce, &signer, &x25519_secret, context).await?;
let quote = create_quote(nonce, &app_state.signer(), &app_state.x25519_secret, context).await?;

// Submit the quote
let attest_tx = entropy::tx().attestation().attest(quote.clone());
submit_transaction(&api, &rpc, &signer, &attest_tx, None).await?;
submit_transaction(&api, &rpc, &app_state.signer(), &attest_tx, None).await?;

Ok(StatusCode::OK)
}
Expand All @@ -94,16 +92,15 @@ pub async fn get_attest(
State(app_state): State<AppState>,
Query(context_querystring): Query<QuoteContextQuery>,
) -> Result<(StatusCode, Vec<u8>), AttestationErr> {
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?;

// Request attestation to get nonce
let nonce = request_attestation(&api, &rpc, signer.signer()).await?;
let nonce = request_attestation(&api, &rpc, &app_state.pair).await?;

let context = context_querystring.as_quote_context()?;

let quote = create_quote(nonce, &signer, &x25519_secret, context).await?;
let quote = create_quote(nonce, &app_state.signer(), &app_state.x25519_secret, context).await?;

Ok((StatusCode::OK, quote))
}
Expand Down
227 changes: 62 additions & 165 deletions crates/threshold-signature-server/src/helpers/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,16 @@

use std::{fs, path::PathBuf};

use crate::{chain_api::entropy, helpers::substrate::query_chain, AppState};
use clap::Parser;
use entropy_client::substrate::SubstrateError;
use entropy_kvdb::{
encrypted_sled::PasswordMethod,
kv_manager::{error::KvError, KvManager},
};
use entropy_shared::NETWORK_PARENT_KEY;
use serde::Deserialize;
use serde_json::json;
use subxt::ext::sp_core::{
crypto::{AccountId32, Ss58Codec},
sr25519, Pair,
};

use crate::helpers::validator::get_signer_and_x25519_secret;
use sp_core::crypto::Ss58Codec;

pub const DEFAULT_MNEMONIC: &str =
"alarm mutual concert decrease hurry invest culture survey diagram crash snap click";
Expand All @@ -53,16 +49,7 @@ pub const LATEST_BLOCK_NUMBER_PROACTIVE_REFRESH: &str = "LATEST_BLOCK_NUMBER_PRO
#[cfg(any(test, feature = "test_helpers"))]
pub const DEFAULT_ENDPOINT: &str = "ws://localhost:9944";

pub const FORBIDDEN_KEYS: [&str; 4] = [
FORBIDDEN_KEY_MNEMONIC,
FORBIDDEN_KEY_SHARED_SECRET,
FORBIDDEN_KEY_DIFFIE_HELLMAN_PUBLIC,
NETWORK_PARENT_KEY,
];

pub const FORBIDDEN_KEY_MNEMONIC: &str = "MNEMONIC";
pub const FORBIDDEN_KEY_SHARED_SECRET: &str = "SHARED_SECRET";
pub const FORBIDDEN_KEY_DIFFIE_HELLMAN_PUBLIC: &str = "DH_PUBLIC";
pub const FORBIDDEN_KEYS: [&str; 1] = [NETWORK_PARENT_KEY];

// Deafult name for TSS server
// Will set mnemonic and db path
Expand Down Expand Up @@ -200,23 +187,6 @@ pub struct StartupArgs {
/// The path to a password file
#[arg(short = 'f', long = "password-file")]
pub password_file: Option<PathBuf>,

/// Set up the key-value store (KVDB), or ensure one already exists, print setup information to
/// stdout, then exit. Supply the `--password-file` option for fully non-interactive operation.
///
/// Returns the AccountID and Diffie-Hellman Public Keys associated with this server.
#[arg(long = "setup-only")]
pub setup_only: bool,
}

pub async fn has_mnemonic(kv: &KvManager) -> bool {
let exists = kv.kv().exists(FORBIDDEN_KEY_MNEMONIC).await.expect("issue querying DB");

if exists {
tracing::debug!("Existing mnemonic found in keystore.");
}

exists
}

pub fn development_mnemonic(validator_name: &Option<ValidatorName>) -> bip39::Mnemonic {
Expand All @@ -236,84 +206,6 @@ pub fn development_mnemonic(validator_name: &Option<ValidatorName>) -> bip39::Mn
.expect("Unable to parse given mnemonic.")
}

pub async fn setup_mnemonic(kv: &KvManager, mnemonic: bip39::Mnemonic) {
if has_mnemonic(kv).await {
tracing::warn!("Deleting account related keys from KVDB.");

kv.kv()
.delete(FORBIDDEN_KEY_MNEMONIC)
.await
.expect("Error deleting existing mnemonic from KVDB.");
kv.kv()
.delete(FORBIDDEN_KEY_SHARED_SECRET)
.await
.expect("Error deleting shared secret from KVDB.");
kv.kv()
.delete(FORBIDDEN_KEY_DIFFIE_HELLMAN_PUBLIC)
.await
.expect("Error deleting X25519 public key from KVDB.");
}

tracing::info!("Writing new mnemonic to KVDB.");

// Write our new mnemonic to the KVDB.
let reservation = kv
.kv()
.reserve_key(FORBIDDEN_KEY_MNEMONIC.to_string())
.await
.expect("Issue reserving mnemonic");
kv.kv()
.put(reservation, mnemonic.to_string().as_bytes().to_vec())
.await
.expect("failed to update mnemonic");

let (pair, static_secret) =
get_signer_and_x25519_secret(kv).await.expect("Cannot derive keypairs");
let x25519_public_key = x25519_dalek::PublicKey::from(&static_secret).to_bytes();

// Write the shared secret in the KVDB
let shared_secret_reservation = kv
.kv()
.reserve_key(FORBIDDEN_KEY_SHARED_SECRET.to_string())
.await
.expect("Issue reserving ss key");
kv.kv()
.put(shared_secret_reservation, static_secret.to_bytes().to_vec())
.await
.expect("failed to update secret share");

// Write the Diffie-Hellman key in the KVDB
let diffie_hellman_reservation = kv
.kv()
.reserve_key(FORBIDDEN_KEY_DIFFIE_HELLMAN_PUBLIC.to_string())
.await
.expect("Issue reserving DH key");

kv.kv()
.put(diffie_hellman_reservation, x25519_public_key.to_vec())
.await
.expect("failed to update dh");

// Now we write the TSS AccountID and X25519 public key to files for convenience reasons.
let formatted_dh_public = format!("{x25519_public_key:?}").replace('"', "");
fs::write(".entropy/public_key", formatted_dh_public).expect("Failed to write public key file");

let id = AccountId32::new(pair.signer().public().0);
fs::write(".entropy/account_id", format!("{id}")).expect("Failed to write account_id file");

tracing::debug!("Starting process with account ID: `{id}`");
}

pub async fn threshold_account_id(kv: &KvManager) -> String {
let mnemonic = kv.kv().get(FORBIDDEN_KEY_MNEMONIC).await.expect("Issue getting mnemonic");
let pair = <sr25519::Pair as Pair>::from_phrase(
&String::from_utf8(mnemonic).expect("Issue converting mnemonic to string"),
None,
)
.expect("Issue converting mnemonic to pair");
AccountId32::new(pair.0.public().into()).to_ss58check()
}

pub async fn setup_latest_block_number(kv: &KvManager) -> Result<(), KvError> {
let exists_result_new_user =
kv.kv().exists(LATEST_BLOCK_NUMBER_NEW_USER).await.expect("issue querying DB");
Expand Down Expand Up @@ -370,27 +262,10 @@ pub async fn setup_latest_block_number(kv: &KvManager) -> Result<(), KvError> {
Ok(())
}

pub async fn setup_only(kv: &KvManager) {
let mnemonic = kv.kv().get(FORBIDDEN_KEYS[0]).await.expect("Issue getting mnemonic");
let pair = <sr25519::Pair as Pair>::from_phrase(
&String::from_utf8(mnemonic).expect("Issue converting mnemonic to string"),
None,
)
.expect("Issue converting mnemonic to pair");
let account_id = AccountId32::new(pair.0.public().into()).to_ss58check();

let dh_public_key = kv.kv().get(FORBIDDEN_KEYS[2]).await.expect("Issue getting dh public key");
let dh_public_key = format!("{dh_public_key:?}").replace('"', "");
let output = json!({
"account_id": account_id,
"dh_public_key": dh_public_key,
});

println!("{}", output);
}

pub async fn check_node_prerequisites(url: &str, account_id: &str) {
pub async fn check_node_prerequisites(app_state: AppState) {
use crate::chain_api::{get_api, get_rpc};
let url = &app_state.configuration.endpoint;
let account_id = app_state.account_id();

let connect_to_substrate_node = || async {
tracing::info!("Attempting to establish connection to Substrate node at `{}`", url);
Expand All @@ -406,45 +281,67 @@ pub async fn check_node_prerequisites(url: &str, account_id: &str) {
Ok((api, rpc))
};

// Note: By default this will wait 15 minutes before it stops retry attempts.
let backoff = backoff::ExponentialBackoff::default();
match backoff::future::retry(backoff, connect_to_substrate_node).await {
// Never give up trying to connect
let backoff = backoff::ExponentialBackoff { max_elapsed_time: None, ..Default::default() };
Copy link
Contributor Author

@ameba23 ameba23 Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not looked at the backoff crate in too much detail but i think adding max_elapsed_time: None means it will keep checking indefinitely.


match backoff::future::retry(backoff.clone(), connect_to_substrate_node).await {
Ok((api, rpc)) => {
tracing::info!("Sucessfully connected to Substrate node!");

tracing::info!("Checking balance of threshold server AccountId `{}`", &account_id);
let balance_query = crate::validator::api::check_balance_for_fees(
&api,
&rpc,
account_id.to_string(),
entropy_shared::MIN_BALANCE,
)
.await
.map_err(|_| Err::<bool, String>("Failed to get balance of account.".to_string()));

match balance_query {
Ok(has_minimum_balance) => {
if has_minimum_balance {
tracing::info!(
"The account `{}` has enough funds for submitting extrinsics.",
&account_id
)
} else {
tracing::warn!(
"The account `{}` does not meet the minimum balance of `{}`",
&account_id,
entropy_shared::MIN_BALANCE,
)
}
},
Err(_) => {
tracing::warn!("Unable to query the account balance of `{}`", &account_id)
},

let balance_query = || async {
let has_minimum_balance = crate::validator::api::check_balance_for_fees(
&api,
&rpc,
account_id.to_ss58check().to_string(),
entropy_shared::MIN_BALANCE,
)
.await
.map_err(|_| {
tracing::error!("Unable to query the account balance of `{}`", &account_id);
"Unable to query account balance".to_string()
})?;
if !has_minimum_balance {
Err("Minimum balance not met".to_string())?
}
Ok(())
};

if let Err(error) = backoff::future::retry(backoff.clone(), balance_query).await {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This balance check and the one below could maybe be combined into a state machine which makes both checks, but i think for now its ok to make one after the other

tracing::error!("This should never happen because backoff has no permanent errors or maximum timeout: {error}");
}

tracing::info!(
"The account `{}` has enough funds for submitting extrinsics.",
&account_id
);

// Now check if there exists a threshold server with our details - if there is not,
// we need to wait until there is
let check_for_tss_account_id = || async {
let stash_address_query = entropy::storage()
.staking_extension()
.threshold_to_stash(subxt::utils::AccountId32(*account_id.as_ref()));

let _stash_address = query_chain(&api, &rpc, stash_address_query, None)
.await?
.ok_or_else(|| SubstrateError::NoEvent)?;
Ok(())
};

tracing::info!(
"Checking if our account ID has been registered on chain `{}`",
&account_id
);
if let Err(error) = backoff::future::retry(backoff, check_for_tss_account_id).await {
tracing::error!("This should never happen because backoff has no permanent errors or maximum timeout: {error}");
}
tracing::info!("TSS node passed all prerequisite checks and is ready");
app_state.make_ready();
},
Err(_err) => {
tracing::error!("Unable to establish connection with Substrate node at `{}`", url);
panic!("Unable to establish connection with Substrate node.");
Err(error) => {
tracing::error!("This should never happen because backoff has no permanent errors or maximum timeout: {error:?}");
},
}
}
Loading
Loading