diff --git a/tools/internal/testnet-manager/migrations/01_initial_tables.sql b/tools/internal/testnet-manager/migrations/01_initial_tables.sql index 6b621e2c99..791a23e07a 100644 --- a/tools/internal/testnet-manager/migrations/01_initial_tables.sql +++ b/tools/internal/testnet-manager/migrations/01_initial_tables.sql @@ -36,5 +36,16 @@ CREATE TABLE contract ( CREATE TABLE account ( address TEXT NOT NULL UNIQUE, + -- for the future 'import' feature this will have to be nullable mnemonic TEXT NOT NULL +); + +CREATE TABLE node ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + identity_key TEXT NOT NULL, + network_id INTEGER NOT NULL REFERENCES network(id), + + -- i.e. mixnode or gateway + bonded_type TEXT NOT NULL, + owner_address TEXT NOT NULL REFERENCES account(address) ); \ No newline at end of file diff --git a/tools/internal/testnet-manager/src/cli/local_nodes.rs b/tools/internal/testnet-manager/src/cli/local_nodes.rs index 54d26fd6fb..2996cc55ff 100644 --- a/tools/internal/testnet-manager/src/cli/local_nodes.rs +++ b/tools/internal/testnet-manager/src/cli/local_nodes.rs @@ -15,6 +15,12 @@ pub(crate) struct Args { #[clap(long)] nym_node_bin: PathBuf, + #[clap(long, default_value_t = 3)] + num_mixnodes: u16, + + #[clap(long, default_value_t = 1)] + num_gateways: u16, + #[clap(long)] network_name: Option, @@ -27,7 +33,12 @@ pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { let network = manager.load_existing_network(args.network_name).await?; let run_cmds = manager - .init_local_nym_nodes(args.nym_node_bin, &network) + .init_local_nym_nodes( + args.nym_node_bin, + &network, + args.num_mixnodes, + args.num_gateways, + ) .await?; if !args.output.is_text() { diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index bd13c8a834..2df9167a0a 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -178,6 +178,9 @@ impl NetworkManager { let id = &ctx.client_id; self.wait_for_gateway(ctx).await?; + let mut rng = thread_rng(); + let mut port = rng.next_u32(); + port = (port + 1000) % (u16::MAX as u32); ctx.set_pb_message(format!("initialising client {id}...")); ctx.println(format!("\tinitialising client {id}...")); @@ -190,6 +193,8 @@ impl NetworkManager { id, "--enabled-credentials-mode", "true", + "--port", + &port.to_string(), ]) .stdout(Stdio::null()) .stdin(Stdio::null()) diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs index 578d72b0fe..ecb3ad8ff2 100644 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -3,17 +3,12 @@ use crate::error::NetworkManagerError; use crate::helpers::{ProgressCtx, ProgressTracker, RunCommands}; -use crate::manager::contract::Account; use crate::manager::network::LoadedNetwork; +use crate::manager::node::NymNode; use crate::manager::NetworkManager; use console::style; -use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{ - construct_gateway_bonding_sign_payload, construct_mixnode_bonding_sign_payload, Addr, Gateway, - Layer, LayerAssignment, MixNode, MixNodeCostParams, Percent, -}; +use nym_mixnet_contract_common::{Layer, LayerAssignment}; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; -use nym_validator_client::nyxd::CosmWasmCoin; use nym_validator_client::DirectSigningHttpRpcNyxdClient; use serde::{Deserialize, Serialize}; use std::fs; @@ -23,99 +18,6 @@ use std::process::Stdio; use tokio::process::Command; use zeroize::Zeroizing; -struct NymNode { - // host is always 127.0.0.1 - mix_port: u16, - verloc_port: u16, - http_port: u16, - clients_port: u16, - sphinx_key: String, - identity_key: String, - version: String, - - owner: Account, - bonding_signature: String, -} - -impl NymNode { - fn new_empty() -> NymNode { - NymNode { - mix_port: 0, - verloc_port: 0, - http_port: 0, - clients_port: 0, - sphinx_key: "".to_string(), - identity_key: "".to_string(), - version: "".to_string(), - owner: Account::new(), - bonding_signature: "".to_string(), - } - } - - fn pledge(&self) -> CosmWasmCoin { - CosmWasmCoin::new(100_000000, "unym") - } - - fn gateway(&self) -> Gateway { - Gateway { - host: "127.0.0.1".to_string(), - mix_port: self.mix_port, - clients_port: self.clients_port, - location: "foomp".to_string(), - sphinx_key: self.sphinx_key.clone(), - identity_key: self.identity_key.clone(), - version: self.version.clone(), - } - } - - fn mixnode(&self) -> MixNode { - MixNode { - host: "127.0.0.1".to_string(), - mix_port: self.mix_port, - verloc_port: self.verloc_port, - http_api_port: self.http_port, - sphinx_key: self.sphinx_key.clone(), - identity_key: self.identity_key.clone(), - version: self.version.clone(), - } - } - - fn cost_params(&self) -> MixNodeCostParams { - MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value(10).unwrap(), - interval_operating_cost: CosmWasmCoin::new(40_000000, "unym"), - } - } - - fn bonding_signature(&self) -> MessageSignature { - // this is a valid bs58 - self.bonding_signature.parse().unwrap() - } - - fn mixnode_bonding_payload(&self) -> String { - let payload = construct_mixnode_bonding_sign_payload( - 0, - Addr::unchecked(self.owner.address.to_string()), - None, - self.pledge(), - self.mixnode(), - self.cost_params(), - ); - payload.to_base58_string().unwrap() - } - - fn gateway_bonding_payload(&self) -> String { - let payload = construct_gateway_bonding_sign_payload( - 0, - Addr::unchecked(self.owner.address.to_string()), - None, - self.pledge(), - self.gateway(), - ); - payload.to_base58_string().unwrap() - } -} - struct LocalNodesCtx<'a> { nym_node_binary: PathBuf, @@ -124,7 +26,7 @@ struct LocalNodesCtx<'a> { admin: DirectSigningHttpRpcNyxdClient, mix_nodes: Vec, - gateway: Option, + gateways: Vec, } impl<'a> ProgressCtx for LocalNodesCtx<'a> { @@ -135,7 +37,7 @@ impl<'a> ProgressCtx for LocalNodesCtx<'a> { impl<'a> LocalNodesCtx<'a> { fn nym_node_id(&self, node: &NymNode) -> String { - format!("{}-{}", node.owner.address, self.network.name) + format!("{}-{}", self.network.name, node.identity_key) } fn new( @@ -158,7 +60,7 @@ impl<'a> LocalNodesCtx<'a> { )?, mix_nodes: Vec::new(), progress, - gateway: None, + gateways: Vec::new(), }) } @@ -335,7 +237,7 @@ impl NetworkManager { )); if is_gateway { - ctx.gateway = Some(node) + ctx.gateways.push(node) } else { ctx.mix_nodes.push(node) } @@ -345,16 +247,24 @@ impl NetworkManager { async fn initialise_nym_nodes<'a>( &self, ctx: &mut LocalNodesCtx<'a>, + num_mixnodes: u16, + num_gateways: u16, ) -> Result<(), NetworkManagerError> { + const OFFSET: u16 = 100; + if num_mixnodes > OFFSET { + panic!("seriously? over 100 mixnodes?") + } + ctx.println(format!( "🔏 {}Initialising local nym-nodes...", - style("[1/4]").bold().dim() + style("[1/5]").bold().dim() )); - // 3 mixnodes, 1 gateway; maybe at some point make it configurable - for i in 0..4 { - let is_gateway = i == 0; - self.initialise_nym_node(ctx, i, is_gateway).await?; + for i in 0..num_mixnodes { + self.initialise_nym_node(ctx, i, false).await?; + } + for i in 0..num_gateways { + self.initialise_nym_node(ctx, i + OFFSET, true).await?; } ctx.println("\t✅ all nym nodes got initialised!"); @@ -368,15 +278,11 @@ impl NetworkManager { ) -> Result<(), NetworkManagerError> { ctx.println(format!( "💸 {}Transferring tokens to the bond owners...", - style("[2/4]").bold().dim() + style("[2/5]").bold().dim() )); let mut receivers = Vec::new(); - for node in ctx - .mix_nodes - .iter() - .chain(std::iter::once(ctx.gateway.as_ref().unwrap())) - { + for node in ctx.mix_nodes.iter().chain(ctx.gateways.iter()) { // send 101nym to the owner receivers.push((node.owner.address.clone(), ctx.admin.mix_coins(101_000000))) } @@ -439,14 +345,15 @@ impl NetworkManager { async fn bond_nym_nodes<'a>(&self, ctx: &LocalNodesCtx<'a>) -> Result<(), NetworkManagerError> { ctx.println(format!( "⛓️ {}Bonding the local nym-nodes...", - style("[3/4]").bold().dim() + style("[3/5]").bold().dim() )); - self.bond_node(ctx, ctx.gateway.as_ref().unwrap(), true) - .await?; for mix_node in &ctx.mix_nodes { self.bond_node(ctx, mix_node, false).await?; } + for gateway in &ctx.gateways { + self.bond_node(ctx, gateway, true).await?; + } ctx.println("\t✅ all nym nodes got bonded!"); @@ -459,7 +366,7 @@ impl NetworkManager { ) -> Result<(), NetworkManagerError> { ctx.println(format!( "🔌 {}Assigning mixnodes to the active set...", - style("[4/4]").bold().dim() + style("[4/5]").bold().dim() )); let rewarder = ctx.signing_rewarder()?; @@ -500,12 +407,23 @@ impl NetworkManager { let env_canon_display = env_canon.display(); let mut cmds = Vec::new(); - for node in ctx - .mix_nodes - .iter() - .chain(std::iter::once(ctx.gateway.as_ref().unwrap())) - { - let id = ctx.nym_node_id(node); + for mixnode in ctx.mix_nodes.iter() { + ctx.println(format!( + "\tpreparing node {} (mixnode)", + mixnode.identity_key + )); + let id = ctx.nym_node_id(mixnode); + cmds.push(format!( + "{bin_canon_display} -c {env_canon_display} run --id {id} --local" + )); + } + + for gateway in ctx.gateways.iter() { + ctx.println(format!( + "\tpreparing node {} (mixnode)", + gateway.identity_key + )); + let id = ctx.nym_node_id(gateway); cmds.push(format!( "{bin_canon_display} -c {env_canon_display} run --id {id} --local" )); @@ -518,10 +436,36 @@ impl NetworkManager { ctx.progress.output_run_commands(cmds) } + async fn persist_nodes_in_database<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📦 {}Storing the node information in the database", + style("[5/5]").bold().dim() + )); + + ctx.set_pb_message("attempting to persist node information..."); + let mix_save_future = self + .storage + .persist_mixnodes(&ctx.mix_nodes, ctx.network.id); + let gw_save_future = self.storage.persist_gateways(&ctx.gateways, ctx.network.id); + ctx.async_with_progress(mix_save_future).await?; + ctx.async_with_progress(gw_save_future).await?; + + ctx.println( + "\t✅ the bonded node information got persisted in the database for future use", + ); + + Ok(()) + } + pub(crate) async fn init_local_nym_nodes>( &self, nym_node_binary: P, network: &LoadedNetwork, + num_mixnodes: u16, + num_gateways: u16, ) -> Result { let mut ctx = LocalNodesCtx::new( nym_node_binary.as_ref().to_path_buf(), @@ -534,10 +478,12 @@ impl NetworkManager { return Err(NetworkManagerError::EnvFileNotGenerated); } - self.initialise_nym_nodes(&mut ctx).await?; + self.initialise_nym_nodes(&mut ctx, num_mixnodes, num_gateways) + .await?; self.transfer_bonding_tokens(&ctx).await?; self.bond_nym_nodes(&ctx).await?; self.assign_to_active_set(&ctx).await?; + self.persist_nodes_in_database(&ctx).await?; let cmds = self.prepare_nym_nodes_run_commands(&ctx)?; self.output_nym_nodes_run_commands(&ctx, &cmds); diff --git a/tools/internal/testnet-manager/src/manager/mod.rs b/tools/internal/testnet-manager/src/manager/mod.rs index 1832ef6cd5..d7499328e2 100644 --- a/tools/internal/testnet-manager/src/manager/mod.rs +++ b/tools/internal/testnet-manager/src/manager/mod.rs @@ -24,6 +24,7 @@ mod local_client; mod local_nodes; pub(crate) mod network; mod network_init; +mod node; pub(crate) mod storage; pub(crate) struct NetworkManager { diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs index 72bc2b1aad..bca7bafa05 100644 --- a/tools/internal/testnet-manager/src/manager/network.rs +++ b/tools/internal/testnet-manager/src/manager/network.rs @@ -34,6 +34,7 @@ impl Network { #[derive(Debug, Serialize, Deserialize)] pub(crate) struct LoadedNetwork { + pub(crate) id: i64, pub(crate) name: String, pub(crate) rpc_endpoint: Url, @@ -49,6 +50,7 @@ pub(crate) struct LoadedNetwork { impl From for LoadedNetwork { fn from(value: Network) -> Self { LoadedNetwork { + id: i64::MAX, name: value.name, rpc_endpoint: value.rpc_endpoint, created_at: value.created_at, diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs index baeb614433..24c3829ee2 100644 --- a/tools/internal/testnet-manager/src/manager/network_init.rs +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -662,7 +662,7 @@ impl NetworkManager { Ok(()) } - async fn persist_in_database(&self, ctx: &InitCtx) -> Result<(), NetworkManagerError> { + async fn persist_network_in_database(&self, ctx: &InitCtx) -> Result<(), NetworkManagerError> { ctx.println(format!( "📦 {}Storing all the results in the database", style("[8/8]").bold().dim() @@ -694,7 +694,7 @@ impl NetworkManager { .await?; self.perform_final_migrations(&mut ctx).await?; self.get_build_info(&mut ctx).await?; - self.persist_in_database(&ctx).await?; + self.persist_network_in_database(&ctx).await?; Ok(ctx.network.clone()) } diff --git a/tools/internal/testnet-manager/src/manager/node.rs b/tools/internal/testnet-manager/src/manager/node.rs new file mode 100644 index 0000000000..3e51b6a36c --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/node.rs @@ -0,0 +1,105 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::manager::contract::Account; +use nym_coconut_dkg_common::types::Addr; +use nym_contracts_common::signing::MessageSignature; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::{ + construct_gateway_bonding_sign_payload, construct_mixnode_bonding_sign_payload, Gateway, + MixNode, MixNodeCostParams, +}; +use nym_validator_client::nyxd::CosmWasmCoin; + +pub(crate) struct NymNode { + // host is always 127.0.0.1 + pub(crate) mix_port: u16, + pub(crate) verloc_port: u16, + pub(crate) http_port: u16, + pub(crate) clients_port: u16, + pub(crate) sphinx_key: String, + pub(crate) identity_key: String, + pub(crate) version: String, + + pub(crate) owner: Account, + pub(crate) bonding_signature: String, +} + +impl NymNode { + pub(crate) fn new_empty() -> NymNode { + NymNode { + mix_port: 0, + verloc_port: 0, + http_port: 0, + clients_port: 0, + sphinx_key: "".to_string(), + identity_key: "".to_string(), + version: "".to_string(), + owner: Account::new(), + bonding_signature: "".to_string(), + } + } + + pub(crate) fn pledge(&self) -> CosmWasmCoin { + CosmWasmCoin::new(100_000000, "unym") + } + + pub(crate) fn gateway(&self) -> Gateway { + Gateway { + host: "127.0.0.1".to_string(), + mix_port: self.mix_port, + clients_port: self.clients_port, + location: "foomp".to_string(), + sphinx_key: self.sphinx_key.clone(), + identity_key: self.identity_key.clone(), + version: self.version.clone(), + } + } + + pub(crate) fn mixnode(&self) -> MixNode { + MixNode { + host: "127.0.0.1".to_string(), + mix_port: self.mix_port, + verloc_port: self.verloc_port, + http_api_port: self.http_port, + sphinx_key: self.sphinx_key.clone(), + identity_key: self.identity_key.clone(), + version: self.version.clone(), + } + } + + pub(crate) fn cost_params(&self) -> MixNodeCostParams { + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: CosmWasmCoin::new(40_000000, "unym"), + } + } + + pub(crate) fn bonding_signature(&self) -> MessageSignature { + // this is a valid bs58 string + self.bonding_signature.parse().unwrap() + } + + pub(crate) fn mixnode_bonding_payload(&self) -> String { + let payload = construct_mixnode_bonding_sign_payload( + 0, + Addr::unchecked(self.owner.address.to_string()), + None, + self.pledge(), + self.mixnode(), + self.cost_params(), + ); + payload.to_base58_string().unwrap() + } + + pub(crate) fn gateway_bonding_payload(&self) -> String { + let payload = construct_gateway_bonding_sign_payload( + 0, + Addr::unchecked(self.owner.address.to_string()), + None, + self.pledge(), + self.gateway(), + ); + payload.to_base58_string().unwrap() + } +} diff --git a/tools/internal/testnet-manager/src/manager/storage/manager.rs b/tools/internal/testnet-manager/src/manager/storage/manager.rs index 7cde58aeed..24132db687 100644 --- a/tools/internal/testnet-manager/src/manager/storage/manager.rs +++ b/tools/internal/testnet-manager/src/manager/storage/manager.rs @@ -180,4 +180,26 @@ impl StorageManager { .fetch_one(&self.connection_pool) .await } + + pub(crate) async fn save_node( + &self, + identity_key: &str, + network_id: i64, + bonded_type: &str, + owner_address: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO node(identity_key, network_id, bonded_type, owner_address) + VALUES (?, ?, ?, ?) + "#, + identity_key, + network_id, + bonded_type, + owner_address + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } } diff --git a/tools/internal/testnet-manager/src/manager/storage/mod.rs b/tools/internal/testnet-manager/src/manager/storage/mod.rs index 8a7960acd4..6faaab0070 100644 --- a/tools/internal/testnet-manager/src/manager/storage/mod.rs +++ b/tools/internal/testnet-manager/src/manager/storage/mod.rs @@ -4,6 +4,7 @@ use std::fs; use crate::error::NetworkManagerError; use crate::manager::contract::{Account, Contract, LoadedNymContracts}; use crate::manager::network::{LoadedNetwork, Network, SpecialAddresses}; +use crate::manager::node::NymNode; use crate::manager::storage::manager::StorageManager; use sqlx::ConnectOptions; use std::path::Path; @@ -117,6 +118,38 @@ impl NetworkManagerStorage { .await?) } + async fn persist_mixnode( + &self, + node: &NymNode, + network_id: i64, + ) -> Result<(), NetworkManagerError> { + Ok(self + .manager + .save_node( + &node.identity_key, + network_id, + "mixnode", + node.owner.address.as_ref(), + ) + .await?) + } + + async fn persist_gateway( + &self, + node: &NymNode, + network_id: i64, + ) -> Result<(), NetworkManagerError> { + Ok(self + .manager + .save_node( + &node.identity_key, + network_id, + "gateway", + node.owner.address.as_ref(), + ) + .await?) + } + async fn persist_account(&self, account: &Account) -> Result<(), NetworkManagerError> { let as_str = Zeroizing::new(account.mnemonic.to_string()); Ok(self @@ -125,6 +158,30 @@ impl NetworkManagerStorage { .await?) } + pub(crate) async fn persist_mixnodes( + &self, + nodes: &[NymNode], + network_id: i64, + ) -> Result<(), NetworkManagerError> { + for node in nodes { + self.persist_account(&node.owner).await?; + self.persist_mixnode(node, network_id).await?; + } + Ok(()) + } + + pub(crate) async fn persist_gateways( + &self, + nodes: &[NymNode], + network_id: i64, + ) -> Result<(), NetworkManagerError> { + for node in nodes { + self.persist_account(&node.owner).await?; + self.persist_gateway(node, network_id).await?; + } + Ok(()) + } + pub(crate) async fn persist_network( &self, network: &Network, @@ -191,6 +248,7 @@ impl NetworkManagerStorage { .ok_or_else(|| NetworkManagerError::RpcEndpointNotSet)?; Ok(LoadedNetwork { + id: base_network.id, name: base_network.name, rpc_endpoint, created_at: base_network.created_at, diff --git a/tools/internal/testnet-manager/src/manager/storage/models.rs b/tools/internal/testnet-manager/src/manager/storage/models.rs index 141b481272..c224fc36d5 100644 --- a/tools/internal/testnet-manager/src/manager/storage/models.rs +++ b/tools/internal/testnet-manager/src/manager/storage/models.rs @@ -60,7 +60,6 @@ impl TryFrom for LoadedContract { #[derive(FromRow)] pub(crate) struct RawNetwork { - #[allow(unused)] pub(crate) id: i64, pub(crate) name: String, pub(crate) created_at: OffsetDateTime,