From 0c2532602a10598e5fb5bf84828f065b21a08c39 Mon Sep 17 00:00:00 2001 From: Fran Domovic <93442516+frdomovic@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:51:06 +0100 Subject: [PATCH 1/3] feat(node): set ICP init local config (#1030) --- crates/merod/src/cli/init.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/merod/src/cli/init.rs b/crates/merod/src/cli/init.rs index 78e92dc38..4d66d5c3b 100644 --- a/crates/merod/src/cli/init.rs +++ b/crates/merod/src/cli/init.rs @@ -222,7 +222,11 @@ impl InitCommand { ContextConfig { client: ClientConfig { signer: ClientSigner { - selected: ClientSelectedSigner::Relayer, + selected: match self.protocol { + ConfigProtocol::Near => ClientSelectedSigner::Relayer, + ConfigProtocol::Starknet => ClientSelectedSigner::Relayer, + ConfigProtocol::Icp => ClientSelectedSigner::Local, + }, relayer: ClientRelayerSigner { url: relayer }, local: LocalConfig { near: [ @@ -286,7 +290,7 @@ impl InitCommand { network: match self.protocol { ConfigProtocol::Near => "testnet".into(), ConfigProtocol::Starknet => "sepolia".into(), - ConfigProtocol::Icp => "ic".into(), + ConfigProtocol::Icp => "local".into(), }, protocol: self.protocol.as_str().to_owned(), contract_id: match self.protocol { From f6ccc8196aea1e501fbcc6294870682ea12df614 Mon Sep 17 00:00:00 2001 From: Matej Vukosav Date: Sat, 21 Dec 2024 07:09:25 +1100 Subject: [PATCH 2/3] feat: implement bootstrap in meroctl (#1016) --- .gitignore | 1 + Cargo.lock | 1 + crates/meroctl/Cargo.toml | 1 + crates/meroctl/src/cli.rs | 14 + crates/meroctl/src/cli/app.rs | 2 +- crates/meroctl/src/cli/app/install.rs | 9 +- crates/meroctl/src/cli/bootstrap.rs | 41 +++ crates/meroctl/src/cli/bootstrap/start.rs | 380 ++++++++++++++++++++++ crates/meroctl/src/cli/context.rs | 6 +- crates/meroctl/src/cli/context/create.rs | 2 +- crates/meroctl/src/cli/context/invite.rs | 14 +- crates/meroctl/src/cli/context/join.rs | 4 +- crates/merod/src/cli/init.rs | 3 +- 13 files changed, 467 insertions(+), 11 deletions(-) create mode 100644 crates/meroctl/src/cli/bootstrap.rs create mode 100644 crates/meroctl/src/cli/bootstrap/start.rs diff --git a/.gitignore b/.gitignore index 136459d76..7fce584ef 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ dist/ lib/ node_modules/ certs/ +output/* diff --git a/Cargo.lock b/Cargo.lock index 6ab88042b..10fc0a7c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4845,6 +4845,7 @@ dependencies = [ "futures-util", "libp2p", "notify", + "rand 0.8.5", "reqwest 0.12.9", "serde", "serde_json", diff --git a/crates/meroctl/Cargo.toml b/crates/meroctl/Cargo.toml index 22f545480..d32d3b970 100644 --- a/crates/meroctl/Cargo.toml +++ b/crates/meroctl/Cargo.toml @@ -20,6 +20,7 @@ eyre.workspace = true futures-util.workspace = true libp2p.workspace = true notify.workspace = true +rand.workspace = true reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true diff --git a/crates/meroctl/src/cli.rs b/crates/meroctl/src/cli.rs index 4c46952b9..ca4027881 100644 --- a/crates/meroctl/src/cli.rs +++ b/crates/meroctl/src/cli.rs @@ -1,5 +1,6 @@ use std::process::ExitCode; +use bootstrap::BootstrapCommand; use camino::Utf8PathBuf; use clap::{Parser, Subcommand}; use const_format::concatcp; @@ -11,6 +12,7 @@ use crate::defaults; use crate::output::{Format, Output, Report}; mod app; +mod bootstrap; mod call; mod context; mod identity; @@ -53,6 +55,7 @@ pub enum SubCommands { Identity(IdentityCommand), Proxy(ProxyCommand), Call(CallCommand), + Bootstrap(BootstrapCommand), } #[derive(Debug, Parser)] @@ -70,6 +73,16 @@ pub struct RootArgs { pub output_format: Format, } +impl RootArgs { + pub const fn new(home: Utf8PathBuf, node_name: String, output_format: Format) -> Self { + Self { + home, + node_name, + output_format, + } + } +} + pub struct Environment { pub args: RootArgs, pub output: Output, @@ -92,6 +105,7 @@ impl RootCommand { SubCommands::Identity(identity) => identity.run(&environment).await, SubCommands::Proxy(proxy) => proxy.run(&environment).await, SubCommands::Call(call) => call.run(&environment).await, + SubCommands::Bootstrap(call) => call.run(&environment).await, }; if let Err(err) = result { diff --git a/crates/meroctl/src/cli/app.rs b/crates/meroctl/src/cli/app.rs index 3878e32df..0e83ccb85 100644 --- a/crates/meroctl/src/cli/app.rs +++ b/crates/meroctl/src/cli/app.rs @@ -10,7 +10,7 @@ use crate::cli::Environment; use crate::output::Report; mod get; -mod install; +pub mod install; mod list; pub const EXAMPLES: &str = r" diff --git a/crates/meroctl/src/cli/app/install.rs b/crates/meroctl/src/cli/app/install.rs index c8487f415..1c3ee166d 100644 --- a/crates/meroctl/src/cli/app/install.rs +++ b/crates/meroctl/src/cli/app/install.rs @@ -1,3 +1,4 @@ +use calimero_primitives::application::ApplicationId; use calimero_primitives::hash::Hash; use calimero_server_primitives::admin::{ InstallApplicationRequest, InstallApplicationResponse, InstallDevApplicationRequest, @@ -36,6 +37,12 @@ impl Report for InstallApplicationResponse { impl InstallCommand { pub async fn run(self, environment: &Environment) -> Result<()> { + let _ignored = self.install_app(environment).await?; + + Ok(()) + } + + pub async fn install_app(self, environment: &Environment) -> Result { let config = load_config(&environment.args.home, &environment.args.node_name)?; let mut is_dev_installation = false; let metadata = self.metadata.map(String::into_bytes).unwrap_or_default(); @@ -76,6 +83,6 @@ impl InstallCommand { environment.output.write(&response); - Ok(()) + Ok(response.data.application_id) } } diff --git a/crates/meroctl/src/cli/bootstrap.rs b/crates/meroctl/src/cli/bootstrap.rs new file mode 100644 index 000000000..dda0d8b65 --- /dev/null +++ b/crates/meroctl/src/cli/bootstrap.rs @@ -0,0 +1,41 @@ +use clap::{Parser, Subcommand}; +use const_format::concatcp; +use eyre::Result as EyreResult; +use start::StartBootstrapCommand; + +use super::Environment; + +mod start; + +pub const EXAMPLES: &str = r" + # Setup and run 2 nodes with demo app + $ meroctl -- --node-name node1 bootstrap start --merod-path /path/to/merod + +# Setup and run 2 nodes with provided app + $ meroctl -- --node-name node1 bootstrap start --merod-path /path/to/merod --app-path /path/to/app + +"; + +#[derive(Debug, Parser)] +#[command(about = "Command for starting bootstrap")] +#[command(after_help = concatcp!( + "Examples:", + EXAMPLES +))] +pub struct BootstrapCommand { + #[command(subcommand)] + pub subcommand: BootstrapSubCommands, +} + +#[derive(Debug, Subcommand)] +pub enum BootstrapSubCommands { + Start(StartBootstrapCommand), +} + +impl BootstrapCommand { + pub async fn run(self, environment: &Environment) -> EyreResult<()> { + match self.subcommand { + BootstrapSubCommands::Start(generate) => generate.run(environment).await, + } + } +} diff --git a/crates/meroctl/src/cli/bootstrap/start.rs b/crates/meroctl/src/cli/bootstrap/start.rs new file mode 100644 index 000000000..33225750f --- /dev/null +++ b/crates/meroctl/src/cli/bootstrap/start.rs @@ -0,0 +1,380 @@ +use std::process::Stdio; +use std::time::Duration; + +use calimero_primitives::application::ApplicationId; +use calimero_primitives::context::ContextId; +use calimero_primitives::hash::Hash; +use calimero_primitives::identity::{PrivateKey, PublicKey}; +use camino::Utf8PathBuf; +use clap::Parser; +use eyre::{bail, Result as EyreResult}; +use reqwest::Client; +use tokio::fs::{create_dir_all, File}; +use tokio::io::copy; +use tokio::process::{Child, Command}; +use tokio::time::sleep; + +use crate::cli::app::install::InstallCommand; +use crate::cli::context::create::create_context; +use crate::cli::context::invite::InviteCommand; +use crate::cli::context::join::JoinCommand; +use crate::cli::{Environment, RootArgs}; +use crate::common::{fetch_multiaddr, load_config}; +use crate::output::Output; + +#[derive(Parser, Debug)] +#[command(about = "Start bootstrap process")] +pub struct StartBootstrapCommand { + #[clap(long, help = "Path to the merod executabe file")] + pub merod_path: Utf8PathBuf, + #[clap(long, help = "Path to the app wasm file")] + pub app_path: Option, +} + +impl StartBootstrapCommand { + pub async fn run(mut self, environment: &Environment) -> EyreResult<()> { + println!("Starting bootstrap process"); + let nodes_dir: Utf8PathBuf = environment.args.home.clone(); + let mut processes: Vec = vec![]; + + // TODO app default from releases + + let mut demo_app = false; + if self.app_path.is_none() { + println!("Downloading demo app..."); + demo_app = true; + + let wasm_url = "https://github.com/calimero-network/core-app-template/raw/refs/heads/master/logic/res/logic.wasm"; + let output_path: Utf8PathBuf = "output/app.wasm".into(); + self.app_path = Some(output_path.clone()); + + if let Err(e) = self.download_wasm(wasm_url, output_path).await { + bail!("Failed to download the WASM file: {:?}", e); + } + } + + let node1_log_dir: Utf8PathBuf = "output/node_1_output".into(); + let node1_name = "node1".to_owned(); + let node1_server_port: u32 = 2428; + let node1_environment = &Environment::new( + RootArgs::new( + nodes_dir.clone(), + node1_name.to_owned(), + crate::output::Format::Json, + ), + Output::new(crate::output::Format::Json), + ); + + let node1_process = self + .initialize_and_start_node( + nodes_dir.to_owned(), + node1_log_dir.to_owned(), + &node1_name, + 2528, + node1_server_port, + ) + .await?; + processes.push(node1_process); + + println!("Creating context in {:?}", node1_name); + let (context_id, public_key, application_id) = + self.create_context_in_bootstrap(node1_environment).await?; + + let node2_name = "node2".to_owned(); + let node2_log_dir: Utf8PathBuf = "output/node_2_output".into(); + let node2_server_port: u32 = 2429; + let node2_environment = &Environment::new( + RootArgs::new( + nodes_dir.clone(), + node2_name.to_owned(), + crate::output::Format::Json, + ), + Output::new(crate::output::Format::Json), + ); + + let node2_process = self + .initialize_and_start_node( + nodes_dir.to_owned(), + node2_log_dir.to_owned(), + &node2_name, + 2529, + node2_server_port, + ) + .await?; + processes.push(node2_process); + + let invitee_private_key = PrivateKey::random(&mut rand::thread_rng()); + + self.invite_and_join_node( + context_id, + public_key, + invitee_private_key, + &node1_environment, + &node2_environment, + ) + .await?; + + println!("************************************************"); + println!("🚀 Bootstrap finished. Nodes are ready to use! 🚀"); + println!("Context id is {:?} ", context_id.to_string(),); + + if demo_app { + println!( + "Connect to the node from https://calimero-network.github.io/core-app-template/" + ); + println!( + "Open application in two separate windows to use it with two different nodes." + ); + println!("Application setup screen requires application id and node url."); + println!("Application id is {:?} ", application_id.to_string(),); + + println!( + "Node {:?} url is http://localhost:{}", + node1_environment.args.node_name, node1_server_port + ); + println!( + "Node {:?} url is http://localhost:{}", + node1_environment.args.node_name, node2_server_port + ); + } + println!("************************************************"); + + self.monitor_processes(processes).await; + + Ok(()) + } + + async fn initialize_and_start_node( + &self, + nodes_dir: Utf8PathBuf, + log_dir: Utf8PathBuf, + node_name: &str, + swarm_port: u32, + server_port: u32, + ) -> EyreResult { + println!("Initializing node {:?}", node_name); + + self.init( + nodes_dir.to_owned(), + log_dir.to_owned(), + node_name.to_owned(), + swarm_port, + server_port, + ) + .await?; + + println!("Starting node {:?}.", node_name); + + let process = self + .run_node(nodes_dir, log_dir, node_name.to_owned()) + .await?; + + sleep(Duration::from_secs(10)).await; + println!("Node {:?} started successfully.", &node_name); + Ok(process) + } + + async fn invite_and_join_node( + &self, + context_id: ContextId, + inviter_public_key: PublicKey, + invitee_private_key: PrivateKey, + invitor_environment: &Environment, + invitee_environment: &Environment, + ) -> EyreResult<()> { + println!( + "Inviting node {:?} to context {:?}", + invitee_environment.args.node_name, + context_id.to_string() + ); + + let invite_command = InviteCommand { + context_id, + inviter_id: inviter_public_key, + invitee_id: invitee_private_key.public_key(), + }; + let invitation_payload = invite_command.invite(invitor_environment).await?; + + println!( + "Node {:?} successfully invited.", + invitee_environment.args.node_name + ); + + println!( + "Joining node {:?} to context.", + invitee_environment.args.node_name + ); + + let join_command = JoinCommand { + private_key: invitee_private_key, + invitation_payload, + }; + join_command.run(invitee_environment).await?; + println!( + "Node {:?} joined successfully.", + invitee_environment.args.node_name + ); + + Ok(()) + } + + pub async fn init( + &self, + nodes_dir: Utf8PathBuf, + log_dir: Utf8PathBuf, + node_name: String, + swarm_port: u32, + server_port: u32, + ) -> EyreResult<()> { + create_dir_all(&nodes_dir.join(&node_name)).await?; + create_dir_all(&log_dir).await?; + + let mut child = self + .run_cmd( + nodes_dir.clone(), + log_dir.clone(), + node_name.clone(), + &[ + "init", + "--swarm-port", + &swarm_port.to_string().as_str(), + "--server-port", + &server_port.to_string().as_str(), + ], + "init", + ) + .await?; + + let result = child.wait().await?; + if !result.success() { + bail!("Failed to initialize node '{}'", node_name); + } + + let mut child = self + .run_cmd(nodes_dir, log_dir, node_name.clone(), &["config"], "config") + .await?; + let result = child.wait().await?; + if !result.success() { + bail!("Failed to configure node '{}'", node_name); + } + Ok(()) + } + + pub async fn run_node( + &self, + nodes_dir: Utf8PathBuf, + log_dir: Utf8PathBuf, + node_name: String, + ) -> EyreResult { + Ok(self + .run_cmd(nodes_dir, log_dir, node_name, &["run"], "run") + .await?) + } + + pub async fn create_context_in_bootstrap( + &self, + environment: &Environment, + ) -> EyreResult<(ContextId, PublicKey, ApplicationId)> { + let config = load_config(&environment.args.home, &environment.args.node_name)?; + let multiaddr = fetch_multiaddr(&config)?; + let client = Client::new(); + + let install_command = InstallCommand { + path: self.app_path.clone(), + url: Some("".to_owned()), + metadata: Some("".to_owned()), + hash: Some(Hash::new("hash".as_bytes())), + }; + + let application_id = install_command.install_app(environment).await?; + + let (context_id, public_key) = create_context( + environment, + &client, + multiaddr, + None, + application_id, + None, + &config.identity, + ) + .await?; + + println!("Context created: {:?}", context_id.as_str()); + + Ok((context_id, public_key, application_id)) + } + + async fn run_cmd( + &self, + nodes_dir: Utf8PathBuf, + log_dir: Utf8PathBuf, + node_name: String, + args: &[&str], + log_suffix: &str, + ) -> EyreResult { + let mut root_args = vec!["--home", &nodes_dir.as_str(), "--node-name", &node_name]; + root_args.extend(args); + + let log_file = log_dir.join(format!("{}.log", log_suffix)); + let mut log_file = File::create(&log_file).await?; + + let mut child = Command::new(&self.merod_path) + .args(root_args) + .stdout(Stdio::piped()) + .spawn()?; + + if let Some(mut stdout) = child.stdout.take() { + drop(tokio::spawn(async move { + if let Err(err) = copy(&mut stdout, &mut log_file).await { + eprintln!("Error copying stdout: {:?}", err); + } + })); + } + + Ok(child) + } + + async fn monitor_processes(&self, mut processes: Vec) { + loop { + for (i, process) in processes.iter_mut().enumerate() { + match process.try_wait() { + Ok(Some(status)) => { + println!("Node {} exited with status: {:?}", i + 1, status); + return; + } + Ok(None) => continue, + Err(e) => { + println!("Error checking node status: {:?}", e); + return; + } + } + } + sleep(Duration::from_secs(1)).await; + } + } + + async fn download_wasm(&self, url: &str, output_path: Utf8PathBuf) -> EyreResult<()> { + let client = Client::new(); + + let response = client + .get(url) + .send() + .await + .map_err(|e| eyre::eyre!("Request failed: {}", e))?; + + if !response.status().is_success() { + bail!("Request failed with status: {}", response.status()); + } + + let mut file = File::create(&output_path) + .await + .map_err(|e| eyre::eyre!("Failed to create file: {}", e))?; + + let _ = copy(&mut response.bytes().await?.as_ref(), &mut file) + .await + .map_err(|e| eyre::eyre!("Failed to copy response bytes: {}", e))?; + + println!("Demo app downloaded successfully."); + Ok(()) + } +} diff --git a/crates/meroctl/src/cli/context.rs b/crates/meroctl/src/cli/context.rs index 47c68c49c..77524c3f3 100644 --- a/crates/meroctl/src/cli/context.rs +++ b/crates/meroctl/src/cli/context.rs @@ -14,11 +14,11 @@ use crate::cli::context::watch::WatchCommand; use crate::cli::Environment; use crate::output::Report; -mod create; +pub mod create; mod delete; mod get; -mod invite; -mod join; +pub mod invite; +pub mod join; mod list; mod update; mod watch; diff --git a/crates/meroctl/src/cli/context/create.rs b/crates/meroctl/src/cli/context/create.rs index ecca61dcf..cb2ace78c 100644 --- a/crates/meroctl/src/cli/context/create.rs +++ b/crates/meroctl/src/cli/context/create.rs @@ -149,7 +149,7 @@ impl CreateCommand { } } -async fn create_context( +pub async fn create_context( environment: &Environment, client: &Client, base_multiaddr: &Multiaddr, diff --git a/crates/meroctl/src/cli/context/invite.rs b/crates/meroctl/src/cli/context/invite.rs index 50babf7d7..274d6092b 100644 --- a/crates/meroctl/src/cli/context/invite.rs +++ b/crates/meroctl/src/cli/context/invite.rs @@ -1,4 +1,4 @@ -use calimero_primitives::context::ContextId; +use calimero_primitives::context::{ContextId, ContextInvitationPayload}; use calimero_primitives::identity::PublicKey; use calimero_server_primitives::admin::{InviteToContextRequest, InviteToContextResponse}; use clap::Parser; @@ -38,6 +38,12 @@ impl Report for InviteToContextResponse { impl InviteCommand { pub async fn run(self, environment: &Environment) -> EyreResult<()> { + let _ignored = self.invite(environment).await?; + + Ok(()) + } + + pub async fn invite(&self, environment: &Environment) -> EyreResult { let config = load_config(&environment.args.home, &environment.args.node_name)?; let response: InviteToContextResponse = do_request( @@ -55,6 +61,10 @@ impl InviteCommand { environment.output.write(&response); - Ok(()) + let invitation_payload = response + .data + .ok_or_else(|| eyre::eyre!("No invitation payload found in the response"))?; + + Ok(invitation_payload) } } diff --git a/crates/meroctl/src/cli/context/join.rs b/crates/meroctl/src/cli/context/join.rs index d25699072..05a8d87c6 100644 --- a/crates/meroctl/src/cli/context/join.rs +++ b/crates/meroctl/src/cli/context/join.rs @@ -16,12 +16,12 @@ pub struct JoinCommand { value_name = "PRIVATE_KEY", help = "The private key for signing the join context request" )] - private_key: PrivateKey, + pub private_key: PrivateKey, #[clap( value_name = "INVITE", help = "The invitation payload for joining the context" )] - invitation_payload: ContextInvitationPayload, + pub invitation_payload: ContextInvitationPayload, } impl Report for JoinContextResponse { diff --git a/crates/merod/src/cli/init.rs b/crates/merod/src/cli/init.rs index 4d66d5c3b..5491caaf8 100644 --- a/crates/merod/src/cli/init.rs +++ b/crates/merod/src/cli/init.rs @@ -157,7 +157,8 @@ impl InitCommand { } } if !self.force { - bail!("Node is already initialized in {:?}", path); + warn!("Node is already initialized in {:?}", path); + return Ok(()); } } From 6f95b905a23d5253a19a514ba3548dc704907065 Mon Sep 17 00:00:00 2001 From: Matej Vukosav Date: Mon, 23 Dec 2024 01:06:40 +1100 Subject: [PATCH 3/3] refactor: update core-templat-app wasm name (#1032) --- crates/meroctl/src/cli/bootstrap/start.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/meroctl/src/cli/bootstrap/start.rs b/crates/meroctl/src/cli/bootstrap/start.rs index 33225750f..dacdd5fe4 100644 --- a/crates/meroctl/src/cli/bootstrap/start.rs +++ b/crates/meroctl/src/cli/bootstrap/start.rs @@ -44,7 +44,7 @@ impl StartBootstrapCommand { println!("Downloading demo app..."); demo_app = true; - let wasm_url = "https://github.com/calimero-network/core-app-template/raw/refs/heads/master/logic/res/logic.wasm"; + let wasm_url = "https://github.com/calimero-network/core-app-template/raw/refs/heads/master/logic/res/increment.wasm"; let output_path: Utf8PathBuf = "output/app.wasm".into(); self.app_path = Some(output_path.clone());