diff --git a/.gitignore b/.gitignore index 97f1e4a..6037552 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ target/ artifacts/ .vscode/ .DS_Store -**/.env.local \ No newline at end of file +**/.env.local +cli/quartz.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d4f6cbc..99d5ffc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,15 @@ dependencies = [ "tungstenite", ] +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -650,6 +659,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" + [[package]] name = "byteorder" version = "1.5.0" @@ -1690,6 +1705,20 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -2536,6 +2565,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "inout" version = "0.1.3" @@ -3309,6 +3344,29 @@ dependencies = [ "hmac", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.72", +] + [[package]] name = "peg" version = "0.8.4" @@ -3525,6 +3583,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", + "version_check", + "yansi", +] + [[package]] name = "prodash" version = "28.0.0" @@ -3631,6 +3702,7 @@ dependencies = [ "cycles-sync", "dirs", "displaydoc", + "figment", "futures-util", "hex", "k256", @@ -3651,6 +3723,7 @@ dependencies = [ "thiserror", "tm-prover", "tokio", + "toml", "tonic 0.12.1", "tracing", "tracing-subscriber", @@ -5498,6 +5571,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -5950,6 +6032,12 @@ dependencies = [ "time", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/apps/transfers/cargo-generate.toml b/apps/transfers/cargo-generate.toml index 895916d..5d51011 100644 --- a/apps/transfers/cargo-generate.toml +++ b/apps/transfers/cargo-generate.toml @@ -1,5 +1,4 @@ [template] -name = "quartz-app-template" description = "An functioning example of a quartz app" ignore = [ @@ -9,5 +8,7 @@ ignore = [ "enclave/target", "enclave/target/*", "frontend/package-lock.json", - "frontend/public/images" + "frontend/public/images", + "trusted.hash", + "trusted.height" ] \ No newline at end of file diff --git a/apps/transfers/quartz.toml b/apps/transfers/quartz.toml new file mode 100644 index 0000000..ae8db57 --- /dev/null +++ b/apps/transfers/quartz.toml @@ -0,0 +1,8 @@ +mock_sgx = false +tx_sender = "admin" +chain_id = "testing" +node_url = "http://127.0.0.1:26657" +enclave_rpc_addr = "http://127.0.0.1" +enclave_rpc_port = 11090 +trusted_hash = "" +trusted_height = 0 \ No newline at end of file diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 5445ef5..8c15d27 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -33,6 +33,8 @@ base64 = "0.22.1" subtle-encoding = "0.5.1" futures-util = "0.3.30" target-lexicon = "0.12.16" +regex = "1.10.5" +toml = "0.8.19" # cosmos cosmrs.workspace = true @@ -46,4 +48,4 @@ tm-prover = { workspace = true} quartz-common = { workspace = true, features=["contract"]} quartz-tee-ra = { workspace = true} mtcs-enclave = { workspace = true, optional = false} -regex = "1.10.5" +figment = { version = "0.10.19", features=["env", "toml"] } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index d5f893b..7787e01 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,12 +1,14 @@ -use std::{env, path::PathBuf}; +use std::path::PathBuf; use clap::{Parser, Subcommand}; use cosmrs::{tendermint::chain::Id as ChainId, AccountId}; +use figment::{providers::Serialized, Figment}; +use serde::{Deserialize, Serialize}; use tracing::metadata::LevelFilter; use crate::handler::utils::helpers::wasmaddr_to_id; -#[derive(clap::Args, Debug, Clone)] +#[derive(clap::Args, Debug, Clone, Serialize)] pub struct Verbosity { /// Increase verbosity, can be repeated up to 2 times #[arg(long, short, action = clap::ArgAction::Count)] @@ -23,61 +25,45 @@ impl Verbosity { } } -#[derive(Debug, Parser)] +#[derive(Debug, Parser, Serialize)] #[command(version, long_about = None)] pub struct Cli { /// Increase log verbosity - #[clap(flatten)] + #[command(flatten)] pub verbose: Verbosity, /// Enable mock SGX mode for testing purposes. /// This flag disables the use of an Intel SGX processor and allows the system to run without remote attestations. - #[clap(long, env)] - pub mock_sgx: bool, + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub mock_sgx: Option, + + /// Path to Quartz app directory + /// Defaults to current working dir + /// For quartz init, root serves as the parent directory of the directory in which the quartz app is generated + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub app_dir: Option, /// Main command #[command(subcommand)] pub command: Command, } -#[derive(Debug, Subcommand)] +#[derive(Debug, Subcommand, Serialize, Clone)] pub enum Command { /// Create an empty Quartz app from a template - Init { - /// the name of your Quartz app directory, defaults to quartz_app - #[clap(long, default_value = "quartz_app")] - name: String, - }, + Init(InitArgs), + /// Perform handshake - Handshake { - /// path to create & init a Quartz app, defaults to current path if unspecified - #[arg(short, long, value_parser = wasmaddr_to_id)] - contract: AccountId, - /// Port enclave is listening on - #[arg(short, long, default_value = "11090")] - port: u16, - /// Name or address of private key with which to sign - #[arg(short, long, default_value = "admin")] - sender: String, - /// The network chain ID - #[arg(long, default_value = "testing")] - chain_id: ChainId, - /// : to tendermint rpc interface for this chain - #[clap(long, default_value_t = default_node_url())] - node_url: String, - /// RPC interface for the Quartz enclave - #[clap(long, default_value_t = default_rpc_addr())] - enclave_rpc_addr: String, - /// Path to Quartz app directory - /// Defaults to current working dir - #[clap(long)] - app_dir: Option, - }, + Handshake(HandshakeArgs), + /// Subcommands for handling the Quartz app contract Contract { #[command(subcommand)] contract_command: ContractCommand, }, + /// Subcommands for handling the Quartz app enclave Enclave { #[command(subcommand)] @@ -85,58 +71,131 @@ pub enum Command { }, } -#[derive(Debug, Clone, Subcommand)] +#[derive(Debug, Clone, Subcommand, Serialize)] pub enum ContractCommand { - Build { - #[clap(long)] - manifest_path: PathBuf, - }, - Deploy { - /// Json-formatted cosmwasm contract initialization message - #[clap(long, default_value = "{}")] - init_msg: String, - /// : to tendermint rpc interface for this chain - #[clap(long, default_value_t = default_node_url())] - node_url: String, - /// Name or address of private key with which to sign - #[arg(short, long, default_value = "admin")] - sender: String, - /// The network chain ID - #[arg(long, default_value = "testing")] - chain_id: ChainId, - /// A human-readable name for this contract in lists - #[arg(long, default_value = "Quartz App Contract")] - label: String, - /// Path to contract wasm binary for deployment - #[clap(long)] - wasm_bin_path: PathBuf, - }, + Build(ContractBuildArgs), + Deploy(ContractDeployArgs), } -#[derive(Debug, Clone, Subcommand)] +#[derive(Debug, Clone, Subcommand, Serialize)] pub enum EnclaveCommand { /// Build the Quartz app's enclave - Build { - /// Path to Cargo.toml file of the Quartz app's enclave package, defaults to './enclave/Cargo.toml' if unspecified - #[arg(long, default_value = "./enclave/Cargo.toml")] - manifest_path: PathBuf, - }, - // Run the Quartz app's enclave - Start { - /// Path to quartz app directory - /// Defaults to current working dir - #[clap(long)] - app_dir: Option, - /// The network chain ID - #[clap(long)] - chain_id: String, - }, + Build(EnclaveBuildArgs), + /// Run the Quartz app's enclave + Start(EnclaveStartArgs), } -fn default_rpc_addr() -> String { - env::var("RPC_URL").unwrap_or_else(|_| "http://127.0.0.1".to_string()) +#[derive(Debug, Parser, Clone, Serialize, Deserialize)] +pub struct InitArgs { + /// The name of your Quartz app directory, defaults to quartz_app + #[arg(default_value = "quartz_app")] + pub name: PathBuf, } -fn default_node_url() -> String { - env::var("NODE_URL").unwrap_or_else(|_| "http://127.0.0.1:26657".to_string()) +#[derive(Debug, Parser, Clone, Serialize, Deserialize)] +pub struct HandshakeArgs { + /// Path to create & init a Quartz app, defaults to current path if unspecified + #[arg(short, long, value_parser = wasmaddr_to_id)] + pub contract: AccountId, + + /// Name or address of private key with which to sign + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub tx_sender: Option, + + /// The network chain ID + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub chain_id: Option, + + /// : to tendermint rpc interface for this chain + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub node_url: Option, + + /// RPC interface for the Quartz enclave + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub enclave_rpc_addr: Option, + + /// Port enclave is listening on + #[arg(long)] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub enclave_rpc_port: Option, +} + +#[derive(Debug, Parser, Clone, Serialize, Deserialize)] +pub struct ContractBuildArgs { + #[arg(long)] + pub manifest_path: PathBuf, +} + +#[derive(Debug, Parser, Clone, Serialize, Deserialize)] +pub struct ContractDeployArgs { + /// Json-formatted cosmwasm contract initialization message + #[arg(long, default_value = "{}")] + pub init_msg: String, + + /// : to tendermint rpc interface for this chain + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub node_url: Option, + + /// Name or address of private key with which to sign + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub tx_sender: Option, + + /// The network chain ID + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub chain_id: Option, + + /// A human-readable name for this contract in lists + #[arg(long, default_value = "Quartz App Contract")] + pub label: String, + + /// Path to contract wasm binary for deployment + #[arg(long)] + pub wasm_bin_path: PathBuf, +} + +#[derive(Debug, Parser, Clone, Serialize, Deserialize)] +pub struct EnclaveBuildArgs { + /// Path to Cargo.toml file of the Quartz app's enclave package, defaults to './enclave/Cargo.toml' if unspecified + #[arg(long, default_value = "./enclave/Cargo.toml")] + pub manifest_path: PathBuf, +} + +#[derive(Debug, Parser, Clone, Serialize, Deserialize)] +pub struct EnclaveStartArgs { + /// The network chain ID + #[arg(long)] + #[serde(skip_serializing_if = "Option::is_none")] + pub chain_id: Option, + + /// Fetch latest trusted hash and height from the chain instead of existing configuration + #[arg(long)] + pub use_latest_trusted: bool, +} + +pub trait ToFigment { + fn to_figment(&self) -> Figment; +} + +impl ToFigment for Command { + fn to_figment(&self) -> Figment { + match self { + Command::Init(args) => Figment::from(Serialized::defaults(args)), + Command::Handshake(args) => Figment::from(Serialized::defaults(args)), + Command::Contract { contract_command } => match contract_command { + ContractCommand::Build(args) => Figment::from(Serialized::defaults(args)), + ContractCommand::Deploy(args) => Figment::from(Serialized::defaults(args)), + }, + Command::Enclave { enclave_command } => match enclave_command { + EnclaveCommand::Build(args) => Figment::from(Serialized::defaults(args)), + EnclaveCommand::Start(args) => Figment::from(Serialized::defaults(args)), + }, + } + } } diff --git a/cli/src/config.rs b/cli/src/config.rs new file mode 100644 index 0000000..11ea896 --- /dev/null +++ b/cli/src/config.rs @@ -0,0 +1,85 @@ +use std::{env, path::PathBuf}; + +use cosmrs::tendermint::chain::Id as ChainId; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Config { + /// Enable mock SGX mode for testing purposes. + /// This flag disables the use of an Intel SGX processor and allows the system to run without remote attestations. + #[serde(default)] + pub mock_sgx: bool, + + /// Name or address of private key with which to sign + #[serde(default = "default_tx_sender")] + pub tx_sender: String, + + /// The network chain ID + #[serde(default = "default_chain_id")] + pub chain_id: ChainId, + + /// : to tendermint rpc interface for this chain + #[serde(default = "default_node_url")] + pub node_url: String, + + /// RPC interface for the Quartz enclave + #[serde(default = "default_rpc_addr")] + pub enclave_rpc_addr: String, + + /// Port enclave is listening on + #[serde(default = "default_port")] + pub enclave_rpc_port: u16, + + /// Path to Quartz app directory + /// Defaults to current working dir + #[serde(default = "default_app_dir")] + pub app_dir: PathBuf, + + /// Trusted height for light client proofs + #[serde(default)] + pub trusted_height: u64, + + /// Trusted hash for block at trusted_height for light client proofs + #[serde(default)] + pub trusted_hash: String, +} + +fn default_rpc_addr() -> String { + env::var("RPC_URL").unwrap_or_else(|_| "http://127.0.0.1".to_string()) +} + +fn default_node_url() -> String { + env::var("NODE_URL").unwrap_or_else(|_| "http://127.0.0.1:26657".to_string()) +} + +fn default_tx_sender() -> String { + String::from("admin") +} + +fn default_chain_id() -> ChainId { + "testing".parse().expect("default chain_id failed") +} + +fn default_port() -> u16 { + 11090 +} + +fn default_app_dir() -> PathBuf { + ".".parse().expect("default app_dir pathbuf failed") +} + +impl Default for Config { + fn default() -> Self { + Config { + mock_sgx: false, + tx_sender: default_tx_sender(), + chain_id: default_chain_id(), + node_url: default_node_url(), + enclave_rpc_addr: default_rpc_addr(), + enclave_rpc_port: default_port(), + app_dir: default_app_dir(), + trusted_height: u64::default(), + trusted_hash: String::default(), + } + } +} diff --git a/cli/src/error.rs b/cli/src/error.rs index db68fd0..0edca56 100644 --- a/cli/src/error.rs +++ b/cli/src/error.rs @@ -9,4 +9,34 @@ pub enum Error { PathNotFile(String), /// unspecified error: {0} GenericErr(String), + /// IoError: {0} + IoError(String), + /// TOML Error : {0} + TomlError(String), + /// Tendermint error: {0} + TendermintError(String), +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::IoError(err.to_string()) + } +} + +impl From for Error { + fn from(err: toml::de::Error) -> Self { + Error::TomlError(err.to_string()) + } +} + +impl From for Error { + fn from(err: toml::ser::Error) -> Self { + Error::TomlError(err.to_string()) + } +} + +impl From for Error { + fn from(err: tendermint::Error) -> Self { + Error::TendermintError(err.to_string()) + } } diff --git a/cli/src/handler.rs b/cli/src/handler.rs index 7dd693c..edc9a62 100644 --- a/cli/src/handler.rs +++ b/cli/src/handler.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use crate::{error::Error, request::Request, response::Response, Config}; +use crate::{config::Config, error::Error, request::Request, response::Response}; pub mod utils; // commands diff --git a/cli/src/handler/contract_build.rs b/cli/src/handler/contract_build.rs index 9ff3f78..d046562 100644 --- a/cli/src/handler/contract_build.rs +++ b/cli/src/handler/contract_build.rs @@ -4,11 +4,11 @@ use async_trait::async_trait; use tracing::{debug, trace}; use crate::{ + config::Config, error::Error, handler::Handler, request::contract_build::ContractBuildRequest, response::{contract_build::ContractBuildResponse, Response}, - Config, }; #[async_trait] diff --git a/cli/src/handler/contract_deploy.rs b/cli/src/handler/contract_deploy.rs index ba1f29b..ac9cad3 100644 --- a/cli/src/handler/contract_deploy.rs +++ b/cli/src/handler/contract_deploy.rs @@ -17,11 +17,11 @@ use super::utils::{ types::{Log, WasmdTxResponse}, }; use crate::{ + config::Config, error::Error, handler::{utils::types::RelayMessage, Handler}, request::contract_deploy::ContractDeployRequest, response::{contract_deploy::ContractDeployResponse, Response}, - Config, }; #[async_trait] @@ -33,11 +33,11 @@ impl Handler for ContractDeployRequest { trace!("initializing directory structure..."); let (code_id, contract_addr) = if config.mock_sgx { - deploy::(self, config.mock_sgx) + deploy::(self, config) .await .map_err(|e| Error::GenericErr(e.to_string()))? } else { - deploy::(self, config.mock_sgx) + deploy::(self, config) .await .map_err(|e| Error::GenericErr(e.to_string()))? }; @@ -52,12 +52,12 @@ impl Handler for ContractDeployRequest { async fn deploy( args: ContractDeployRequest, - mock_sgx: bool, + config: Config, ) -> Result<(u64, String), anyhow::Error> { // TODO: Replace with call to Rust package let relay_path = current_dir()?.join("../"); - let httpurl = Url::parse(&format!("http://{}", args.node_url))?; + let httpurl = Url::parse(&format!("http://{}", config.node_url))?; let tmrpc_client = HttpClient::new(httpurl.as_str())?; let wasmd_client = CliWasmdClient::new(Url::parse(httpurl.as_str())?); @@ -67,8 +67,8 @@ async fn deploy( // TODO: uncertain about the path -> string conversion let deploy_output: WasmdTxResponse = serde_json::from_str(&wasmd_client.deploy( - &args.chain_id, - &args.sender, + &config.chain_id, + &config.tx_sender, contract_path.display().to_string(), )?)?; let res = block_tx_commit(&tmrpc_client, deploy_output.txhash).await?; @@ -79,17 +79,18 @@ async fn deploy( info!("\nšŸš€ Communicating with Relay to Instantiate...\n"); let raw_init_msg = run_relay::>( relay_path.as_path(), - mock_sgx, + config.mock_sgx, RelayMessage::Instantiate, - )?; + ) + .await?; info!("\nšŸš€ Instantiating {} Contract\n", args.label); let mut init_msg = args.init_msg; init_msg["quartz"] = json!(raw_init_msg); let init_output: WasmdTxResponse = serde_json::from_str(&wasmd_client.init( - &args.chain_id, - &args.sender, + &config.chain_id, + &config.tx_sender, code_id, json!(init_msg), &format!("{} Contract #{}", args.label, code_id), diff --git a/cli/src/handler/enclave_build.rs b/cli/src/handler/enclave_build.rs index cd888e0..451e689 100644 --- a/cli/src/handler/enclave_build.rs +++ b/cli/src/handler/enclave_build.rs @@ -4,11 +4,11 @@ use async_trait::async_trait; use tracing::{debug, info}; use crate::{ + config::Config, error::Error, handler::Handler, request::enclave_build::EnclaveBuildRequest, response::{enclave_build::EnclaveBuildResponse, Response}, - Config, }; #[async_trait] diff --git a/cli/src/handler/enclave_start.rs b/cli/src/handler/enclave_start.rs index 0caa797..4a4c9ae 100644 --- a/cli/src/handler/enclave_start.rs +++ b/cli/src/handler/enclave_start.rs @@ -4,13 +4,12 @@ use async_trait::async_trait; use tokio::process::Command; use tracing::{debug, info}; -use super::utils::helpers::read_hash_height; use crate::{ + config::Config, error::Error, - handler::Handler, + handler::{utils::helpers::get_hash_height, Handler}, request::enclave_start::EnclaveStartRequest, response::{enclave_start::EnclaveStartResponse, Response}, - Config, }; #[async_trait] @@ -18,16 +17,16 @@ impl Handler for EnclaveStartRequest { type Error = Error; type Response = Response; - async fn handle(self, config: Config) -> Result { - let enclave_dir = self.app_dir.join("enclave"); - let (trusted_height, trusted_hash) = read_hash_height(self.app_dir.as_path()) - .await - .map_err(|e| Error::GenericErr(e.to_string()))?; + async fn handle(self, mut config: Config) -> Result { + // Get trusted height and hash + let (trusted_height, trusted_hash) = get_hash_height(self.use_latest_trusted, &mut config)?; + + let enclave_dir = config.app_dir.join("enclave"); if config.mock_sgx { let enclave_args: Vec = vec![ "--chain-id".to_string(), - self.chain_id, + config.chain_id.to_string(), "--trusted-height".to_string(), trusted_height.to_string(), "--trusted-hash".to_string(), diff --git a/cli/src/handler/handshake.rs b/cli/src/handler/handshake.rs index ed9d6d1..0f67ecd 100644 --- a/cli/src/handler/handshake.rs +++ b/cli/src/handler/handshake.rs @@ -17,14 +17,14 @@ use super::utils::{ types::WasmdTxResponse, }; use crate::{ + config::Config, error::Error, handler::{ - utils::{helpers::read_hash_height, types::RelayMessage}, + utils::{helpers::get_hash_height, types::RelayMessage}, Handler, }, request::handshake::HandshakeRequest, response::{handshake::HandshakeResponse, Response}, - Config, }; #[async_trait] @@ -36,7 +36,7 @@ impl Handler for HandshakeRequest { trace!("starting handshake..."); // TODO: may need to import verbosity here - let pub_key = handshake(self, config.mock_sgx) + let pub_key = handshake(self, config) .await .map_err(|e| Error::GenericErr(e.to_string()))?; @@ -49,30 +49,32 @@ struct Message<'a> { message: &'a str, } -async fn handshake(args: HandshakeRequest, mock_sgx: bool) -> Result { - let httpurl = Url::parse(&format!("http://{}", args.node_url))?; - let wsurl = format!("ws://{}/websocket", args.node_url); +async fn handshake(args: HandshakeRequest, mut config: Config) -> Result { + let httpurl = Url::parse(&format!("http://{}", config.node_url))?; + let wsurl = format!("ws://{}/websocket", config.node_url); let tmrpc_client = HttpClient::new(httpurl.as_str())?; let wasmd_client = CliWasmdClient::new(Url::parse(httpurl.as_str())?); + let (trusted_height, trusted_hash) = get_hash_height(false, &mut config)?; // TODO: dir logic issue #125 - // Read trusted hash and height from files let base_path = current_dir()?.join("../"); - let trusted_files_path = args.app_dir; - let (trusted_height, trusted_hash) = read_hash_height(trusted_files_path.as_path()).await?; info!("Running SessionCreate"); - let res: serde_json::Value = - run_relay(base_path.as_path(), mock_sgx, RelayMessage::SessionCreate)?; + let res: serde_json::Value = run_relay( + base_path.as_path(), + config.mock_sgx, + RelayMessage::SessionCreate, + ) + .await?; let output: WasmdTxResponse = serde_json::from_str( wasmd_client .tx_execute( &args.contract.clone(), - &args.chain_id, + &config.chain_id, 2000000, - &args.sender, + &config.tx_sender, json!(res), )? .as_str(), @@ -92,7 +94,7 @@ async fn handshake(args: HandshakeRequest, mock_sgx: bool) -> Result Result Result Result Result { + async fn handle(self, config: Config) -> Result { trace!("initializing directory structure..."); let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".."); + let parent = self + .name + .parent() + .map(|p| p.to_path_buf()) + .expect("path already validated"); + fs::create_dir_all(&parent) + .await + .map_err(|e| Error::GenericErr(e.to_string()))?; + + let file_name = self + .name + .file_name() + .and_then(|f| f.to_str()) + .expect("path already validated"); + let wasm_pack_args = GenerateArgs { - name: Some(self.name), + name: Some(file_name.to_string()), + destination: Some(config.app_dir.join(parent)), + overwrite: true, vcs: Some(Vcs::Git), template_path: TemplatePath { // git: Some("git@github.com:informalsystems/cycles-quartz.git".to_string()), // TODO: replace with public http address when open-sourced diff --git a/cli/src/handler/utils/helpers.rs b/cli/src/handler/utils/helpers.rs index 7a3c253..ecebfdf 100644 --- a/cli/src/handler/utils/helpers.rs +++ b/cli/src/handler/utils/helpers.rs @@ -1,18 +1,21 @@ -use std::{path::Path, process::Command, time::Duration}; +use std::{path::Path, time::Duration}; use anyhow::anyhow; use cosmrs::{AccountId, ErrorReport}; +use cycles_sync::wasmd_client::{CliWasmdClient, WasmdClient}; use regex::Regex; +use reqwest::Url; use serde::de::DeserializeOwned; use subtle_encoding::bech32::decode as bech32_decode; use tendermint::{block::Height, Hash}; use tendermint_rpc::{ endpoint::tx::Response as TmTxResponse, error::ErrorDetail, Client, HttpClient, }; -use tokio::fs; +use tokio::{fs, process::Command}; use tracing::debug; use super::types::RelayMessage; +use crate::{config::Config, error}; pub fn wasmaddr_to_id(address_str: &str) -> Result { let (hr, _) = bech32_decode(address_str).map_err(|e| anyhow!(e))?; @@ -24,7 +27,7 @@ pub fn wasmaddr_to_id(address_str: &str) -> Result { } // TODO: move wrapping result with "quartz:" struct into here -pub fn run_relay( +pub async fn run_relay( base_path: &Path, mock_sgx: bool, msg: RelayMessage, @@ -41,7 +44,7 @@ pub fn run_relay( command.arg(proof); } - let output = command.output()?; + let output = command.output().await?; if !output.status.success() { return Err(anyhow!("{:?}", output)); @@ -87,18 +90,55 @@ pub async fn block_tx_commit(client: &HttpClient, tx: Hash) -> Result Result<(Height, Hash), anyhow::Error> { - let height_path = base_path.join("trusted.height"); - let trusted_height: Height = fs::read_to_string(height_path.as_path()) - .await? - .trim() - .parse()?; +/// Returns the trusted hash and height +pub fn get_hash_height( + use_latest: bool, + config: &mut Config, +) -> Result<(Height, Hash), error::Error> { + if use_latest || config.trusted_height == 0 || config.trusted_hash.is_empty() { + let (trusted_height, trusted_hash) = latest_height_hash(&config.node_url)?; + config.trusted_hash = trusted_hash.to_string(); + config.trusted_height = trusted_height.into(); - let hash_path = base_path.join("trusted.hash"); - let trusted_hash: Hash = fs::read_to_string(hash_path.as_path()) - .await? - .trim() - .parse()?; - - Ok((trusted_height, trusted_hash)) + Ok((trusted_height, trusted_hash)) + } else { + Ok(( + config.trusted_height.try_into()?, + config + .trusted_hash + .parse() + .map_err(|_| error::Error::GenericErr("invalid hash".to_string()))?, + )) + } +} + +// Queries the chain for the latested height and hash +pub fn latest_height_hash(node_url: &String) -> Result<(Height, Hash), error::Error> { + let httpurl = Url::parse(&format!("http://{}", node_url)) + .map_err(|e| error::Error::GenericErr(e.to_string()))?; + let wasmd_client = CliWasmdClient::new(httpurl); + + let (trusted_height, trusted_hash) = wasmd_client + .trusted_height_hash() + .map_err(|e| error::Error::GenericErr(e.to_string()))?; + + Ok(( + trusted_height.try_into()?, + trusted_hash.parse().expect("invalid hash from wasmd"), + )) +} + +pub async fn persist_config_hash_height(config: &Config) -> Result<(), error::Error> { + let config_path = config.app_dir.join("quartz.toml"); + + let toml_content = fs::read_to_string(&config_path).await?; + let mut written_config: Config = toml::from_str(&toml_content)?; + + written_config.trusted_hash.clone_from(&config.trusted_hash); + written_config.trusted_height = config.trusted_height; + + let toml_string = toml::to_string(config)?; + fs::write(&config_path, toml_string).await?; + + Ok(()) } diff --git a/cli/src/main.rs b/cli/src/main.rs index c471f8e..31825da 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -14,13 +14,22 @@ )] pub mod cli; +pub mod config; pub mod error; pub mod handler; pub mod request; pub mod response; +use std::path::PathBuf; + use clap::Parser; +use cli::ToFigment; use color_eyre::eyre::Result; +use config::Config; +use figment::{ + providers::{Env, Format, Serialized, Toml}, + Figment, +}; use tracing_subscriber::{util::SubscriberInitExt, EnvFilter}; use crate::{cli::Cli, handler::Handler, request::Request}; @@ -37,17 +46,26 @@ const BANNER: &str = r" "; -pub struct Config { - pub mock_sgx: bool, -} - #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; println!("{BANNER}"); - let args = Cli::parse(); + let args: Cli = Cli::parse(); + check_path(&args.app_dir)?; + + let config: Config = Figment::new() + .merge(Toml::file( + args.app_dir + .as_ref() + .unwrap_or(&PathBuf::from(".")) + .join("quartz.toml"), + )) + .merge(Env::prefixed("QUARTZ_")) + .merge(Serialized::defaults(&args)) + .merge(args.command.to_figment()) + .extract()?; let env_filter = EnvFilter::builder() .with_default_directive(args.verbose.to_level_filter().into()) @@ -67,11 +85,7 @@ async fn main() -> Result<()> { // Each `Request` defines an associated `Handler` (i.e. logic) and `Response`. All handlers are // free to log to the terminal and these logs are sent to `stderr`. - let response = request - .handle(Config { - mock_sgx: args.mock_sgx, - }) - .await?; + let response = request.handle(config).await?; // `Handlers` must use `Responses` to output to `stdout`. println!( @@ -81,3 +95,13 @@ async fn main() -> Result<()> { Ok(()) } + +fn check_path(path: &Option) -> Result<(), error::Error> { + if let Some(path) = path { + if !path.is_dir() { + return Err(error::Error::PathNotDir(format!("{}", path.display()))); + } + } + + Ok(()) +} diff --git a/cli/src/request.rs b/cli/src/request.rs index 8a33389..c421ed9 100644 --- a/cli/src/request.rs +++ b/cli/src/request.rs @@ -1,5 +1,3 @@ -use std::{env::current_dir, path::PathBuf}; - use crate::{ cli::{Command, ContractCommand, EnclaveCommand}, error::Error, @@ -32,23 +30,9 @@ impl TryFrom for Request { fn try_from(cmd: Command) -> Result { match cmd { - Command::Init { name } => Ok(InitRequest { name }.try_into()?), - Command::Handshake { - contract, - port, - sender, - chain_id, - node_url, - enclave_rpc_addr, - app_dir, - } => Ok(HandshakeRequest { - contract, - port, - sender, - chain_id, - node_url, - enclave_rpc_addr, - app_dir: Self::path_checked(app_dir)?, + Command::Init(args) => Ok(InitRequest { name: args.name }.try_into()?), + Command::Handshake(args) => Ok(HandshakeRequest { + contract: args.contract, } .into()), Command::Contract { contract_command } => contract_command.try_into(), @@ -57,53 +41,33 @@ impl TryFrom for Request { } } -impl Request { - fn path_checked(path: Option) -> Result { - if let Some(path) = path { - if !path.is_dir() { - return Err(Error::PathNotDir(format!("{}", path.display()))); - } - Ok(path) - } else { - Ok(current_dir().map_err(|e| Error::GenericErr(e.to_string()))?) - } - } -} - impl TryFrom for Request { type Error = Error; fn try_from(cmd: ContractCommand) -> Result { match cmd { - ContractCommand::Deploy { - init_msg, - node_url, - chain_id, - sender, - label, - wasm_bin_path, - } => { - if !wasm_bin_path.exists() { - return Err(Error::PathNotFile(wasm_bin_path.display().to_string())); + ContractCommand::Deploy(args) => { + if !args.wasm_bin_path.exists() { + return Err(Error::PathNotFile(args.wasm_bin_path.display().to_string())); } Ok(ContractDeployRequest { - init_msg: serde_json::from_str(&init_msg) + init_msg: serde_json::from_str(&args.init_msg) .map_err(|e| Error::GenericErr(e.to_string()))?, - node_url, - chain_id, - sender, - label, - wasm_bin_path, + label: args.label, + wasm_bin_path: args.wasm_bin_path, } .into()) } - ContractCommand::Build { manifest_path } => { - if !manifest_path.exists() { - return Err(Error::PathNotFile(manifest_path.display().to_string())); + ContractCommand::Build(args) => { + if !args.manifest_path.exists() { + return Err(Error::PathNotFile(args.manifest_path.display().to_string())); } - Ok(ContractBuildRequest { manifest_path }.into()) + Ok(ContractBuildRequest { + manifest_path: args.manifest_path, + } + .into()) } } } @@ -114,12 +78,12 @@ impl TryFrom for Request { fn try_from(cmd: EnclaveCommand) -> Result { match cmd { - EnclaveCommand::Build { manifest_path } => { - Ok(EnclaveBuildRequest { manifest_path }.into()) + EnclaveCommand::Build(args) => Ok(EnclaveBuildRequest { + manifest_path: args.manifest_path, } - EnclaveCommand::Start { app_dir, chain_id } => Ok(EnclaveStartRequest { - app_dir: Self::path_checked(app_dir)?, - chain_id, + .into()), + EnclaveCommand::Start(args) => Ok(EnclaveStartRequest { + use_latest_trusted: args.use_latest_trusted, } .into()), } diff --git a/cli/src/request/contract_deploy.rs b/cli/src/request/contract_deploy.rs index 35e294f..0db66e9 100644 --- a/cli/src/request/contract_deploy.rs +++ b/cli/src/request/contract_deploy.rs @@ -1,6 +1,5 @@ use std::{collections::HashMap, path::PathBuf}; -use cosmrs::tendermint::chain::Id as ChainId; use serde::{Deserialize, Serialize}; use crate::{error::Error, request::Request}; @@ -8,9 +7,6 @@ use crate::{error::Error, request::Request}; #[derive(Clone, Debug)] pub struct ContractDeployRequest { pub init_msg: serde_json::Value, - pub node_url: String, - pub chain_id: ChainId, - pub sender: String, pub label: String, pub wasm_bin_path: PathBuf, } diff --git a/cli/src/request/enclave_start.rs b/cli/src/request/enclave_start.rs index 272c3dd..c5ab5bd 100644 --- a/cli/src/request/enclave_start.rs +++ b/cli/src/request/enclave_start.rs @@ -1,11 +1,8 @@ -use std::path::PathBuf; - use crate::request::Request; #[derive(Clone, Debug)] pub struct EnclaveStartRequest { - pub app_dir: PathBuf, - pub chain_id: String, + pub use_latest_trusted: bool, } impl From for Request { diff --git a/cli/src/request/handshake.rs b/cli/src/request/handshake.rs index 2b3a321..9f57bf8 100644 --- a/cli/src/request/handshake.rs +++ b/cli/src/request/handshake.rs @@ -1,18 +1,10 @@ -use std::path::PathBuf; - -use cosmrs::{tendermint::chain::Id as ChainId, AccountId}; +use cosmrs::AccountId; use crate::request::Request; #[derive(Clone, Debug)] pub struct HandshakeRequest { pub contract: AccountId, - pub port: u16, - pub sender: String, - pub chain_id: ChainId, - pub node_url: String, - pub enclave_rpc_addr: String, - pub app_dir: PathBuf, } impl From for Request { diff --git a/cli/src/request/init.rs b/cli/src/request/init.rs index 2c276f9..51f5ee9 100644 --- a/cli/src/request/init.rs +++ b/cli/src/request/init.rs @@ -1,18 +1,23 @@ -use std::path::Path; +use std::path::PathBuf; use crate::{error::Error, request::Request}; #[derive(Clone, Debug)] pub struct InitRequest { - pub name: String, + pub name: PathBuf, } impl TryFrom for Request { type Error = Error; fn try_from(request: InitRequest) -> Result { - if Path::new(&request.name).iter().count() != 1 { - return Err(Error::GenericErr("App name contains path".to_string())); + if request.name.extension().is_some() { + return Err(Error::PathNotDir(format!("{}", request.name.display()))); + } else if request.name.exists() { + return Err(Error::GenericErr(format!( + "Directory already exists: {}", + request.name.display() + ))); } Ok(Request::Init(request)) diff --git a/utils/cycles-sync/src/wasmd_client.rs b/utils/cycles-sync/src/wasmd_client.rs index d1c8b11..2329aeb 100644 --- a/utils/cycles-sync/src/wasmd_client.rs +++ b/utils/cycles-sync/src/wasmd_client.rs @@ -51,6 +51,8 @@ pub trait WasmdClient { init_msg: M, label: &str, ) -> Result; + + fn trusted_height_hash(&self) -> Result<(u64, String), Self::Error>; } #[derive(Clone, Debug, Serialize, Deserialize, Default)] @@ -227,4 +229,29 @@ impl WasmdClient for CliWasmdClient { // TODO: find the rust type for the tx output and return that Ok((String::from_utf8(output.stdout)?).to_string()) } + + fn trusted_height_hash(&self) -> Result<(u64, String), Self::Error> { + let mut wasmd = Command::new("wasmd"); + let command = wasmd.args(["--node", self.url.as_str()]).arg("status"); + + let output = command.output()?; + + if !output.status.success() { + return Err(anyhow!("{:?}", output)); + } + + let query_result: serde_json::Value = + serde_json::from_slice(&output.stdout).unwrap_or_default(); + + let trusted_height = query_result["SyncInfo"]["latest_block_height"] + .as_u64() + .ok_or(anyhow!("Could not query height"))?; + + let trusted_hash = query_result["SyncInfo"]["latest_block_hash"] + .as_str() + .ok_or(anyhow!("Could not query height"))? + .to_string(); + + Ok((trusted_height, trusted_hash)) + } }