diff --git a/CHANGELOG.md b/CHANGELOG.md index f64774980..3c1670223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ runtime - Protocol message versioning ([#1140](https://github.com/entropyxyz/entropy-core/pull/1140)) - CLI command to get oracle headings ([#1170](https://github.com/entropyxyz/entropy-core/pull/1170)) - Add TSS endpoint to get TDX quote ([#1173](https://github.com/entropyxyz/entropy-core/pull/1173)) +- Test CLI command to retrieve quote and change endpoint / TSS account in one command ([#1198](https://github.com/entropyxyz/entropy-core/pull/1198)) ### Changed - Use correct key rotation endpoint in OCW ([#1104](https://github.com/entropyxyz/entropy-core/pull/1104)) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 062de590a..71a5c240f 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -17,13 +17,12 @@ //! Used in integration tests and for the test-cli pub use crate::{ chain_api::{get_api, get_rpc}, - errors::ClientError, + errors::{ClientError, SubstrateError}, }; -use anyhow::anyhow; pub use entropy_protocol::{sign_and_encrypt::EncryptedSignedMessage, KeyParams}; +pub use entropy_shared::{HashingAlgorithm, QuoteContext}; use parity_scale_codec::Decode; use rand::Rng; -use std::str::FromStr; pub use synedrion::KeyShare; use crate::{ @@ -39,7 +38,7 @@ use crate::{ EntropyConfig, }, client::entropy::staking_extension::events::{EndpointChanged, ThresholdAccountChanged}, - substrate::{get_registered_details, submit_transaction_with_pair}, + substrate::{get_registered_details, query_chain, submit_transaction_with_pair}, user::{ self, get_all_signers_from_chain, get_validators_not_signer_for_relay, UserSignatureRequest, }, @@ -48,7 +47,6 @@ use crate::{ use base64::prelude::{Engine, BASE64_STANDARD}; use entropy_protocol::RecoverableSignature; -use entropy_shared::HashingAlgorithm; use futures::stream::StreamExt; use sp_core::{ sr25519::{self, Signature}, @@ -338,50 +336,90 @@ pub async fn put_register_request_on_chain( registered_event } -/// Changes the endpoint of a validator +/// Changes the endpoint of a validator, retrieving a TDX quote from the new endpoint internally +pub async fn get_quote_and_change_endpoint( + api: &OnlineClient, + rpc: &LegacyRpcMethods, + validator_keypair: sr25519::Pair, + new_endpoint: String, +) -> Result { + let quote = get_tdx_quote(&new_endpoint, QuoteContext::ChangeEndpoint).await?; + change_endpoint(api, rpc, validator_keypair, new_endpoint, quote).await +} + +/// Changes the endpoint of a validator, with a TDX quote given as an argument pub async fn change_endpoint( api: &OnlineClient, rpc: &LegacyRpcMethods, user_keypair: sr25519::Pair, new_endpoint: String, quote: Vec, -) -> anyhow::Result { +) -> Result { let change_endpoint_tx = entropy::tx().staking_extension().change_endpoint(new_endpoint.into(), quote); let in_block = submit_transaction_with_pair(api, rpc, &user_keypair, &change_endpoint_tx, None).await?; let result_event = in_block .find_first::()? - .ok_or(anyhow!("Error with transaction"))?; + .ok_or(SubstrateError::NoEvent)?; Ok(result_event) } +/// Changes the threshold account info of a validator, retrieving a TDX quote from the new endpoint internally +pub async fn get_quote_and_change_threshold_accounts( + api: &OnlineClient, + rpc: &LegacyRpcMethods, + validator_keypair: sr25519::Pair, + new_tss_account: SubxtAccountId32, + new_x25519_public_key: [u8; 32], + new_pck_certificate_chain: Vec>, +) -> Result { + let quote = get_tdx_quote_with_validator_id( + api, + rpc, + &SubxtAccountId32(validator_keypair.public().0), + QuoteContext::ChangeThresholdAccounts, + ) + .await?; + change_threshold_accounts( + api, + rpc, + validator_keypair, + new_tss_account, + new_x25519_public_key, + new_pck_certificate_chain, + quote, + ) + .await +} + /// Changes the threshold account info of a validator pub async fn change_threshold_accounts( api: &OnlineClient, rpc: &LegacyRpcMethods, - user_keypair: sr25519::Pair, - new_tss_account: String, - new_x25519_public_key: String, + validator_keypair: sr25519::Pair, + new_tss_account: SubxtAccountId32, + new_x25519_public_key: [u8; 32], new_pck_certificate_chain: Vec>, quote: Vec, -) -> anyhow::Result { - let tss_account = SubxtAccountId32::from_str(&new_tss_account)?; - let x25519_public_key = hex::decode(new_x25519_public_key)? - .try_into() - .map_err(|_| anyhow!("X25519 pub key needs to be 32 bytes"))?; +) -> Result { let change_threshold_accounts = entropy::tx().staking_extension().change_threshold_accounts( - tss_account, - x25519_public_key, + new_tss_account, + new_x25519_public_key, new_pck_certificate_chain, quote, ); - let in_block = - submit_transaction_with_pair(api, rpc, &user_keypair, &change_threshold_accounts, None) - .await?; + let in_block = submit_transaction_with_pair( + api, + rpc, + &validator_keypair, + &change_threshold_accounts, + None, + ) + .await?; let result_event = in_block .find_first::()? - .ok_or(anyhow!("Error with transaction"))?; + .ok_or(SubstrateError::NoEvent)?; Ok(result_event) } @@ -463,3 +501,31 @@ pub async fn get_oracle_headings( } Ok(headings) } + +/// Retrieve a TDX quote using the currently configured endpoint associated with the given validator +/// ID +pub async fn get_tdx_quote_with_validator_id( + api: &OnlineClient, + rpc: &LegacyRpcMethods, + validator_stash: &SubxtAccountId32, + quote_context: QuoteContext, +) -> Result, ClientError> { + let query = entropy::storage().staking_extension().threshold_servers(validator_stash); + let server_info = query_chain(api, rpc, query, None).await?.ok_or(ClientError::NoServerInfo)?; + + let tss_endpoint = std::str::from_utf8(&server_info.endpoint)?; + get_tdx_quote(tss_endpoint, quote_context).await +} + +/// Retrieve a TDX quote with a given socket address +pub async fn get_tdx_quote( + tss_endpoint: &str, + quote_context: QuoteContext, +) -> Result, ClientError> { + let response = + reqwest::get(format!("http://{}/attest?context={}", tss_endpoint, quote_context)).await?; + if response.status() != reqwest::StatusCode::OK { + return Err(ClientError::QuoteGet(response.text().await?)); + } + Ok(response.bytes().await?.to_vec()) +} diff --git a/crates/client/src/errors.rs b/crates/client/src/errors.rs index c2f4ab241..61cd74d8f 100644 --- a/crates/client/src/errors.rs +++ b/crates/client/src/errors.rs @@ -123,4 +123,8 @@ pub enum ClientError { Codec(#[from] parity_scale_codec::Error), #[error("Attestation request: {0}")] AttestationRequest(#[from] AttestationRequestError), + #[error("Unable to get TDX quote: {0}")] + QuoteGet(String), + #[error("Unable to get info for TSS server from chain")] + NoServerInfo, } diff --git a/crates/client/src/tests.rs b/crates/client/src/tests.rs index 52abd3329..a42b7758e 100644 --- a/crates/client/src/tests.rs +++ b/crates/client/src/tests.rs @@ -141,8 +141,8 @@ async fn test_change_threshold_accounts() { &api, &rpc, one.into(), - tss_public_key.to_string(), - hex::encode(*x25519_public_key.as_bytes()), + tss_public_key.into(), + *x25519_public_key.as_bytes(), pck_certificate_chain, quote, ) diff --git a/crates/shared/src/types.rs b/crates/shared/src/types.rs index d6156a0be..2de1b647c 100644 --- a/crates/shared/src/types.rs +++ b/crates/shared/src/types.rs @@ -146,6 +146,18 @@ pub enum QuoteContext { ChangeThresholdAccounts, } +#[cfg(feature = "std")] +impl std::fmt::Display for QuoteContext { + /// Custom display implementation so that it can be used to build a query string + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + QuoteContext::Validate => write!(f, "validate"), + QuoteContext::ChangeEndpoint => write!(f, "change_endpoint"), + QuoteContext::ChangeThresholdAccounts => write!(f, "change_threshold_accounts"), + } + } +} + /// A trait for types which can handle attestation requests. #[cfg(not(feature = "wasm"))] pub trait AttestationHandler { diff --git a/crates/test-cli/src/lib.rs b/crates/test-cli/src/lib.rs index 8b76fe5f8..42a030545 100644 --- a/crates/test-cli/src/lib.rs +++ b/crates/test-cli/src/lib.rs @@ -15,7 +15,7 @@ //! Simple CLI to test registering, updating programs and signing use anyhow::{anyhow, ensure}; -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; use colored::Colorize; use entropy_client::{ chain_api::{ @@ -27,15 +27,15 @@ use entropy_client::{ EntropyConfig, }, client::{ - change_endpoint, change_threshold_accounts, get_accounts, get_api, get_oracle_headings, - get_programs, get_rpc, jumpstart_network, register, remove_program, sign, store_program, - update_programs, VERIFYING_KEY_LENGTH, + get_accounts, get_api, get_oracle_headings, get_programs, get_quote_and_change_endpoint, + get_quote_and_change_threshold_accounts, get_rpc, get_tdx_quote, jumpstart_network, + register, remove_program, sign, store_program, update_programs, VERIFYING_KEY_LENGTH, }, }; -pub use entropy_shared::PROGRAM_VERSION_NUMBER; +pub use entropy_shared::{QuoteContext, PROGRAM_VERSION_NUMBER}; use sp_core::{sr25519, Hasher, Pair}; use sp_runtime::{traits::BlakeTwo256, Serialize}; -use std::{fs, path::PathBuf}; +use std::{fs, path::PathBuf, str::FromStr}; use subxt::{ backend::legacy::LegacyRpcMethods, utils::{AccountId32 as SubxtAccountId32, H256}, @@ -150,11 +150,6 @@ enum CliCommand { ChangeEndpoint { /// New endpoint to change to (ex. "127.0.0.1:3001") new_endpoint: String, - /// The Intel TDX quote used to prove that this TSS is running on TDX hardware. - /// - /// The quote format is specified in: - /// https://download.01.org/intel-sgx/latest/dcap-latest/linux/docs/Intel_TDX_DCAP_Quoting_Library_API.pdf - quote: String, /// The mnemonic for the validator stash account to use for the call, should be stash address #[arg(short, long)] mnemonic_option: Option, @@ -167,11 +162,6 @@ enum CliCommand { new_x25519_public_key: String, /// The new Provisioning Certification Key (PCK) certificate chain to be used for the TSS. new_pck_certificate_chain: Vec, - /// The Intel TDX quote used to prove that this TSS is running on TDX hardware. - /// - /// The quote format is specified in: - /// https://download.01.org/intel-sgx/latest/dcap-latest/linux/docs/Intel_TDX_DCAP_Quoting_Library_API.pdf - quote: String, /// The mnemonic for the validator stash account to use for the call, should be stash address #[arg(short, long)] mnemonic_option: Option, @@ -197,6 +187,9 @@ enum CliCommand { GetTdxQuote { /// The socket address of the TS server, eg: `127.0.0.1:3002` tss_endpoint: String, + /// The context in which this quote will be used. Must be one of + #[arg(value_enum)] + quote_context: QuoteContextArg, /// The filename to write the quote to. Defaults to `quote.dat` #[arg(long)] output_filename: Option, @@ -222,20 +215,12 @@ pub async fn run_command( std::env::var("ENTROPY_DEVNET").unwrap_or("ws://localhost:9944".to_string()) }); - let passed_mnemonic = std::env::var("DEPLOYER_MNEMONIC"); - let api = get_api(&endpoint_addr).await?; let rpc = get_rpc(&endpoint_addr).await?; match cli.command.clone() { CliCommand::Register { mnemonic_option, programs, program_version_numbers } => { - let mnemonic = if let Some(mnemonic_option) = mnemonic_option { - mnemonic_option - } else { - passed_mnemonic.expect("No mnemonic set") - }; - - let program_keypair = ::from_string(&mnemonic, None)?; + let program_keypair = handle_mnemonic(mnemonic_option)?; let program_account = SubxtAccountId32(program_keypair.public().0); cli.log(format!("Program account: {}", program_keypair.public())); @@ -274,13 +259,9 @@ pub async fn run_command( } }, CliCommand::Sign { signature_verifying_key, message, auxilary_data, mnemonic_option } => { - let mnemonic = if let Some(mnemonic_option) = mnemonic_option { - mnemonic_option - } else { - passed_mnemonic.unwrap_or("//Alice".to_string()) - }; // If an account name is not provided, use the Alice key - let user_keypair = ::from_string(&mnemonic, None)?; + let user_keypair = handle_mnemonic(mnemonic_option) + .unwrap_or(::from_string("//Alice", None)?); cli.log(format!("User account for current call: {}", user_keypair.public())); @@ -315,12 +296,7 @@ pub async fn run_command( aux_data_interface_file, program_version_number, } => { - let mnemonic = if let Some(mnemonic_option) = mnemonic_option { - mnemonic_option - } else { - passed_mnemonic.expect("No Mnemonic set") - }; - let keypair = ::from_string(&mnemonic, None)?; + let keypair = handle_mnemonic(mnemonic_option)?; cli.log(format!("Storing program using account: {}", keypair.public())); let program = match program_file { @@ -370,12 +346,7 @@ pub async fn run_command( } }, CliCommand::RemoveProgram { mnemonic_option, hash } => { - let mnemonic = if let Some(mnemonic_option) = mnemonic_option { - mnemonic_option - } else { - passed_mnemonic.expect("No Mnemonic set") - }; - let keypair = ::from_string(&mnemonic, None)?; + let keypair = handle_mnemonic(mnemonic_option)?; cli.log(format!("Removing program using account: {}", keypair.public())); let hash: [u8; 32] = hex::decode(hash)? @@ -396,12 +367,7 @@ pub async fn run_command( programs, program_version_numbers, } => { - let mnemonic = if let Some(mnemonic_option) = mnemonic_option { - mnemonic_option - } else { - passed_mnemonic.expect("No Mnemonic set") - }; - let program_keypair = ::from_string(&mnemonic, None)?; + let program_keypair = handle_mnemonic(mnemonic_option)?; cli.log(format!("Program account: {}", program_keypair.public())); let mut programs_info = Vec::new(); @@ -499,18 +465,12 @@ pub async fn run_command( Ok("Got status".to_string()) } }, - CliCommand::ChangeEndpoint { new_endpoint, quote, mnemonic_option } => { - let mnemonic = if let Some(mnemonic_option) = mnemonic_option { - mnemonic_option - } else { - passed_mnemonic.expect("No Mnemonic set") - }; - - let user_keypair = ::from_string(&mnemonic, None)?; + CliCommand::ChangeEndpoint { new_endpoint, mnemonic_option } => { + let user_keypair = handle_mnemonic(mnemonic_option)?; cli.log(format!("User account for current call: {}", user_keypair.public())); let result_event = - change_endpoint(&api, &rpc, user_keypair, new_endpoint, quote.into()).await?; + get_quote_and_change_endpoint(&api, &rpc, user_keypair, new_endpoint).await?; cli.log(format!("Event result: {:?}", result_event)); if cli.json { @@ -523,27 +483,24 @@ pub async fn run_command( new_tss_account, new_x25519_public_key, new_pck_certificate_chain, - quote, mnemonic_option, } => { - let mnemonic = if let Some(mnemonic_option) = mnemonic_option { - mnemonic_option - } else { - passed_mnemonic.expect("No Mnemonic set") - }; - let user_keypair = ::from_string(&mnemonic, None)?; + let user_keypair = handle_mnemonic(mnemonic_option)?; cli.log(format!("User account for current call: {}", user_keypair.public())); + let new_tss_account = SubxtAccountId32::from_str(&new_tss_account)?; + let new_x25519_public_key = hex::decode(new_x25519_public_key)? + .try_into() + .map_err(|_| anyhow!("X25519 pub key needs to be 32 bytes"))?; let new_pck_certificate_chain = new_pck_certificate_chain.iter().cloned().map(|i| i.into()).collect::<_>(); - let result_event = change_threshold_accounts( + let result_event = get_quote_and_change_threshold_accounts( &api, &rpc, user_keypair, new_tss_account, new_x25519_public_key, new_pck_certificate_chain, - quote.into(), ) .await?; cli.log(format!("Event result: {:?}", result_event)); @@ -555,13 +512,7 @@ pub async fn run_command( } }, CliCommand::JumpstartNetwork { mnemonic_option } => { - let mnemonic = if let Some(mnemonic_option) = mnemonic_option { - mnemonic_option - } else { - passed_mnemonic.unwrap_or("//Alice".to_string()) - }; - - let signer = ::from_string(&mnemonic, None)?; + let signer = handle_mnemonic(mnemonic_option)?; cli.log(format!("Account being used for jumpstart: {}", signer.public())); jumpstart_network(&api, &rpc, signer).await?; @@ -576,9 +527,8 @@ pub async fn run_command( let headings = get_oracle_headings(&api, &rpc).await?; Ok(serde_json::to_string_pretty(&headings)?) }, - CliCommand::GetTdxQuote { tss_endpoint, output_filename } => { - let quote_bytes = - reqwest::get(format!("http://{}/attest", tss_endpoint)).await?.bytes().await?; + CliCommand::GetTdxQuote { tss_endpoint, output_filename, quote_context } => { + let quote_bytes = get_tdx_quote(&tss_endpoint, quote_context.into()).await?; let output_filename = output_filename.unwrap_or("quote.dat".into()); std::fs::write(&output_filename, quote_bytes)?; @@ -713,3 +663,35 @@ impl StatusOutput { Self { accounts, programs } } } + +/// Get an sr25519 from a mnemonic given as either option or environment variable +fn handle_mnemonic(mnemonic_option: Option) -> anyhow::Result { + let mnemonic = if let Some(mnemonic) = mnemonic_option { + mnemonic + } else { + std::env::var("DEPLOYER_MNEMONIC") + .map_err(|_| anyhow!("A mnemonic must be given either by the command line option or DEPLOYER_MNEMONIC environment variable"))? + }; + Ok(::from_string(&mnemonic, None)?) +} + +/// This is the same as [QuoteContext] but implements [ValueEnum] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] +enum QuoteContextArg { + /// To be used in the `validate` extrinsic + Validate, + /// To be used in the `change_endpoint` extrinsic + ChangeEndpoint, + /// To be used in the `change_threshold_accounts` extrinsic + ChangeThresholdAccounts, +} + +impl From for QuoteContext { + fn from(quote_context: QuoteContextArg) -> Self { + match quote_context { + QuoteContextArg::Validate => QuoteContext::Validate, + QuoteContextArg::ChangeEndpoint => QuoteContext::ChangeEndpoint, + QuoteContextArg::ChangeThresholdAccounts => QuoteContext::ChangeThresholdAccounts, + } + } +}