From 5074b8a0953d8b899ae42831aaf58b36ce29e242 Mon Sep 17 00:00:00 2001 From: Gauthier Leonard Date: Thu, 8 Feb 2024 16:53:07 +0100 Subject: [PATCH 1/4] feat: use AddPermissionlessValidatorTx --- crates/ash_cli/src/avalanche/blockchain.rs | 2 +- crates/ash_cli/src/avalanche/subnet.rs | 2 +- crates/ash_cli/src/avalanche/validator.rs | 70 +++++++++++++++++++--- crates/ash_cli/src/avalanche/wallet.rs | 2 +- crates/ash_cli/src/avalanche/x.rs | 2 +- crates/ash_sdk/src/avalanche/nodes.rs | 3 +- crates/ash_sdk/src/avalanche/subnets.rs | 22 +++---- crates/ash_sdk/src/avalanche/txs/p.rs | 30 +++++++--- 8 files changed, 97 insertions(+), 36 deletions(-) diff --git a/crates/ash_cli/src/avalanche/blockchain.rs b/crates/ash_cli/src/avalanche/blockchain.rs index 951a65b..4e7dee2 100644 --- a/crates/ash_cli/src/avalanche/blockchain.rs +++ b/crates/ash_cli/src/avalanche/blockchain.rs @@ -60,7 +60,7 @@ enum BlockchainSubcommands { /// Private key to sign the transaction with (must be a control key) #[arg(long, short = 'p', env = "AVALANCHE_PRIVATE_KEY")] private_key: String, - /// Private key format + /// Private key encoding (cb58 or hex) #[arg( long, short = 'e', diff --git a/crates/ash_cli/src/avalanche/subnet.rs b/crates/ash_cli/src/avalanche/subnet.rs index 36aab9d..1e6fb01 100644 --- a/crates/ash_cli/src/avalanche/subnet.rs +++ b/crates/ash_cli/src/avalanche/subnet.rs @@ -48,7 +48,7 @@ enum SubnetSubcommands { /// Private key to sign the transaction with #[arg(long, short = 'p', env = "AVALANCHE_PRIVATE_KEY")] private_key: String, - /// Private key format + /// Private key encoding (cb58 or hex) #[arg( long, short = 'e', diff --git a/crates/ash_cli/src/avalanche/validator.rs b/crates/ash_cli/src/avalanche/validator.rs index f26405d..ff96ddb 100644 --- a/crates/ash_cli/src/avalanche/validator.rs +++ b/crates/ash_cli/src/avalanche/validator.rs @@ -7,9 +7,20 @@ use crate::{ avalanche::{wallet::*, *}, utils::{error::CliError, parsing::*, templating::*, version_tx_cmd}, }; -use ash_sdk::avalanche::{subnets::AvalancheSubnetType, AVAX_PRIMARY_NETWORK_ID}; +use ash_sdk::avalanche::{ + nodes::ProofOfPossession, subnets::AvalancheSubnetType, AVAX_PRIMARY_NETWORK_ID, +}; use async_std::task; -use clap::{Parser, Subcommand}; +use chrono::Utc; +use clap::{Parser, Subcommand, ValueEnum}; +use std::fmt::Display; + +/// Node signer format +#[derive(Display, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub(crate) enum SignerFormat { + Str, + Json, +} /// Interact with Avalanche validators #[derive(Parser)] @@ -45,19 +56,19 @@ enum ValidatorSubcommands { id: String, /// Validator weight (permissioned Subnet) or stake in AVAX (elastic Subnet) stake_or_weight: u64, - /// Start time of the validation (YYYY-MM-DDTHH:MM:SSZ) + /// Start time of the validation (YYYY-MM-DDTHH:MM:SSZ), defaults to now #[arg(long, short = 'S')] - start_time: String, + start_time: Option, /// End time of the validation (YYYY-MM-DDTHH:MM:SSZ) #[arg(long, short = 'E')] end_time: String, - /// Delegation fee (percentage) + /// Delegation fee (percentage), defaults to 2% #[arg(long, short = 'f', default_value = "2")] delegation_fee: u32, /// Private key to sign the transaction with #[arg(long, short = 'p', env = "AVALANCHE_PRIVATE_KEY")] private_key: String, - /// Private key format + /// Private key encoding (cb58 or hex) #[arg( long, short = 'e', @@ -65,6 +76,13 @@ enum ValidatorSubcommands { env = "AVALANCHE_KEY_ENCODING" )] key_encoding: PrivateKeyEncoding, + /// Signer (BLS public key and PoP) in "public_key:PoP" or JSON format + /// (e.g. '{"publicKey":"public_key","proofOfPossession":"pop"}') + #[arg(long, short = 'B')] + signer: Option, + /// Signer format (str or json) + #[arg(long, short = 'F', default_value = "str")] + signer_format: SignerFormat, /// Whether to wait for transaction acceptance #[arg(long, short = 'w')] wait: bool, @@ -176,18 +194,43 @@ fn add( subnet_id: &str, id: &str, stake_or_weight: u64, - start_time: String, + start_time: Option, end_time: String, delegation_fee: u32, private_key: &str, key_encoding: PrivateKeyEncoding, + signer: Option, + signer_format: SignerFormat, wait: bool, config: Option<&str>, json: bool, ) -> Result<(), CliError> { let node_id_parsed = parse_node_id(id)?; - let start_time_parsed = parse_datetime(&start_time)?; + let start_time_parsed = match start_time { + Some(start_time) => parse_datetime(&start_time)?, + None => Utc::now(), + }; let end_time_parsed = parse_datetime(&end_time)?; + let signer_parsed = match signer.clone() { + Some(signer_str) => match signer_format { + SignerFormat::Str => { + let parts: Vec<&str> = signer_str.split(':').collect(); + if parts.len() != 2 { + return Err(CliError::dataerr( + "Signer must be in the format 'public_key:PoP'".to_string(), + )); + } + serde_json::from_value::(serde_json::json!({ + "publicKey": parts[0], + "proofOfPossession": parts[1] + })) + .map_err(|e| CliError::dataerr(format!("Error parsing signer: {e}")))? + } + SignerFormat::Json => serde_json::from_str(&signer_str) + .map_err(|e| CliError::dataerr(format!("Error parsing signer: {e}")))?, + }, + None => ProofOfPossession::default(), + }; let mut network = load_network(network_name, config)?; update_network_subnets(&mut network)?; @@ -204,14 +247,19 @@ fn add( let validator = match subnet.subnet_type { AvalancheSubnetType::PrimaryNetwork => task::block_on(async { subnet - .add_avalanche_validator( + .add_validator_permissionless( &wallet, node_id_parsed, + subnet.id, // Multiply by 1 billion to convert from AVAX to nAVAX stake_or_weight * 1_000_000_000, start_time_parsed, end_time_parsed, delegation_fee, + match signer { + Some(_) => Some(signer_parsed), + None => None, + }, wait, ) .await @@ -261,6 +309,8 @@ pub(crate) fn parse( delegation_fee, private_key, key_encoding, + signer, + signer_format, wait, } => add( &validator.network, @@ -272,6 +322,8 @@ pub(crate) fn parse( delegation_fee, &private_key, key_encoding, + signer, + signer_format, wait, config, json, diff --git a/crates/ash_cli/src/avalanche/wallet.rs b/crates/ash_cli/src/avalanche/wallet.rs index 120375b..ec81ba7 100644 --- a/crates/ash_cli/src/avalanche/wallet.rs +++ b/crates/ash_cli/src/avalanche/wallet.rs @@ -36,7 +36,7 @@ enum WalletSubcommands { /// Private key of the wallet #[arg(env = "AVALANCHE_PRIVATE_KEY")] private_key: String, - /// Private key format + /// Private key encoding (cb58 or hex) #[arg( long, short = 'e', diff --git a/crates/ash_cli/src/avalanche/x.rs b/crates/ash_cli/src/avalanche/x.rs index 7045f06..ea50e2c 100644 --- a/crates/ash_cli/src/avalanche/x.rs +++ b/crates/ash_cli/src/avalanche/x.rs @@ -53,7 +53,7 @@ enum XSubcommands { /// Private key to sign the transaction with #[arg(long, short = 'p', env = "AVALANCHE_PRIVATE_KEY")] private_key: String, - /// Private key format + /// Private key encoding (cb58 or hex) #[arg( long, short = 'e', diff --git a/crates/ash_sdk/src/avalanche/nodes.rs b/crates/ash_sdk/src/avalanche/nodes.rs index 7860196..50f582a 100644 --- a/crates/ash_sdk/src/avalanche/nodes.rs +++ b/crates/ash_sdk/src/avalanche/nodes.rs @@ -4,11 +4,10 @@ // Module that contains code to interact with Avalanche nodes use crate::{avalanche::jsonrpc::info::*, errors::*}; -pub use avalanche_types::key::bls::private_key::Key as BlsPrivateKey; +pub use avalanche_types::key::bls::{private_key::Key as BlsPrivateKey, ProofOfPossession}; use avalanche_types::{ ids::node::Id as NodeId, jsonrpc::info::{GetNodeVersionResult, UptimeResult, VmVersions}, - key::bls::ProofOfPossession, }; use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, PKCS_RSA_SHA256}; use rustls_pemfile::certs; diff --git a/crates/ash_sdk/src/avalanche/subnets.rs b/crates/ash_sdk/src/avalanche/subnets.rs index fc97e53..eac5ca2 100644 --- a/crates/ash_sdk/src/avalanche/subnets.rs +++ b/crates/ash_sdk/src/avalanche/subnets.rs @@ -18,6 +18,7 @@ use crate::{ use avalanche_types::{ ids::{node::Id as NodeId, Id}, jsonrpc::platformvm::{ApiPrimaryDelegator, ApiPrimaryValidator}, + key::bls::ProofOfPossession, utils::urls::extract_scheme_host_port_path_chain_alias, }; use chrono::{DateTime, Utc}; @@ -121,35 +122,28 @@ impl AvalancheSubnet { }) } - /// Add a validator to the Primary Network - /// Fail if the Subnet is not the Primary Network - pub async fn add_avalanche_validator( + /// Add a validator a permissionless Subnet + pub async fn add_validator_permissionless( &self, wallet: &AvalancheWallet, node_id: NodeId, + subnet_id: Id, stake_amount: u64, start_time: DateTime, end_time: DateTime, reward_fee_percent: u32, + signer: Option, check_acceptance: bool, ) -> Result { - // Check if the Subnet is the Primary Network - if self.subnet_type != AvalancheSubnetType::PrimaryNetwork { - return Err(AvalancheSubnetError::OperationNotAllowed { - operation: "add_avalanche_validator".to_string(), - subnet_id: self.id.to_string(), - subnet_type: self.subnet_type.to_string(), - } - .into()); - } - - let tx_id = p::add_avalanche_validator( + let tx_id = p::add_permissionless_subnet_validator( wallet, node_id, + subnet_id, stake_amount, start_time, end_time, reward_fee_percent, + signer, check_acceptance, ) .await?; diff --git a/crates/ash_sdk/src/avalanche/txs/p.rs b/crates/ash_sdk/src/avalanche/txs/p.rs index ddffbee..32524ba 100644 --- a/crates/ash_sdk/src/avalanche/txs/p.rs +++ b/crates/ash_sdk/src/avalanche/txs/p.rs @@ -3,9 +3,13 @@ // Module that contains code to issue transactions on the X-Chain -use crate::{avalanche::wallets::AvalancheWallet, errors::*}; +use crate::{ + avalanche::{wallets::AvalancheWallet, AVAX_PRIMARY_NETWORK_ID}, + errors::*, +}; use avalanche_types::{ ids::{node::Id as NodeId, Id}, + key::bls::ProofOfPossession, wallet::p, }; use chrono::{DateTime, Duration, Utc}; @@ -56,7 +60,7 @@ pub async fn create_blockchain( Ok(tx_id) } -/// Add a validator to the Primary Network +/// Add a validator to a permissioned Subnet pub async fn add_permissioned_subnet_validator( wallet: &AvalancheWallet, subnet_id: Id, @@ -109,22 +113,30 @@ pub async fn add_permissioned_subnet_validator( } } -/// Add a validator to the Avalanche Primary Network -pub async fn add_avalanche_validator( +/// Add a validator to a permissionless Subnet (e.g. Primary Network) +pub async fn add_permissionless_subnet_validator( wallet: &AvalancheWallet, node_id: NodeId, + subnet_id: Id, stake_amount: u64, start_time: DateTime, end_time: DateTime, reward_fee_percent: u32, + signer: Option, check_acceptance: bool, ) -> Result { - let (tx_id, success) = p::add_validator::Tx::new(&wallet.pchain_wallet.p()) + let (tx_id, success) = p::add_permissionless_validator::Tx::new(&wallet.pchain_wallet.p()) .node_id(node_id) + // avalanche-types requires the subnet_id to be empty for the Primary Network + .subnet_id(match subnet_id.to_string().as_str() { + AVAX_PRIMARY_NETWORK_ID => Id::empty(), + _ => subnet_id, + }) .stake_amount(stake_amount) .start_time(start_time) .end_time(end_time) .reward_fee_percent(reward_fee_percent) + .proof_of_possession(signer.unwrap_or_default()) .check_acceptance(check_acceptance) .poll_initial_wait(Duration::seconds(1).to_std().unwrap()) .issue() @@ -164,7 +176,8 @@ pub async fn add_avalanche_validator( mod tests { use super::*; use crate::avalanche::{ - vms::{encode_genesis_data, AvalancheVmType, subnet_evm::AVAX_SUBNET_EVM_ID}, + nodes::generate_node_bls_key, + vms::{encode_genesis_data, subnet_evm::AVAX_SUBNET_EVM_ID, AvalancheVmType}, AvalancheNetwork, }; use chrono::Duration; @@ -279,13 +292,16 @@ mod tests { assert_eq!(subnet_validator.unwrap().weight, Some(100)); // Try to add a validator that already exists on the Primary Network - let avalanche_validator = add_avalanche_validator( + let (_, pop) = generate_node_bls_key().unwrap(); + let avalanche_validator = add_permissionless_subnet_validator( &local_wallet, NodeId::from_str(NETWORK_RUNNER_NODE_ID).unwrap(), + Id::from_str(AVAX_PRIMARY_NETWORK_ID).unwrap(), 1 * 1_000_000_000, start_time, end_time, 2, + Some(pop), true, ) .await; From 98eca8f5a393d58043696ee138732856b3b18dac Mon Sep 17 00:00:00 2001 From: Gauthier Leonard Date: Thu, 8 Feb 2024 17:00:15 +0100 Subject: [PATCH 2/4] feat: bump to version 0.4.1-rc.1 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/ash_cli/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ab214d..e8c5825 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,7 +175,7 @@ dependencies = [ [[package]] name = "ash_cli" -version = "0.4.0" +version = "0.4.1-rc.1" dependencies = [ "ash_sdk", "async-std", @@ -204,7 +204,7 @@ dependencies = [ [[package]] name = "ash_sdk" -version = "0.4.0" +version = "0.4.1-rc.1" dependencies = [ "ash_api", "async-std", diff --git a/Cargo.toml b/Cargo.toml index 4e7f040..b23ccdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["crates/ash_cli", "crates/ash_sdk"] [workspace.package] -version = "0.4.0" +version = "0.4.1-rc.1" edition = "2021" authors = ["E36 Knots"] homepage = "https://ash.center" diff --git a/crates/ash_cli/Cargo.toml b/crates/ash_cli/Cargo.toml index 9f5fe9a..da77dd9 100644 --- a/crates/ash_cli/Cargo.toml +++ b/crates/ash_cli/Cargo.toml @@ -15,7 +15,7 @@ categories.workspace = true keywords.workspace = true [dependencies] -ash_sdk = { path = "../ash_sdk", version = "0.4.0" } +ash_sdk = { path = "../ash_sdk", version = "0.4.1-rc.1" } clap = { version = "4.0.32", features = ["derive", "env", "cargo", "string"] } colored = "2.0.0" exitcode = "1.1.2" From 1a2998db46d63bf64f3b8effd7ab53f35ddb20a3 Mon Sep 17 00:00:00 2001 From: Gauthier Leonard Date: Tue, 13 Feb 2024 10:51:11 +0100 Subject: [PATCH 3/4] chore: specify resolver in workspace Cargo.toml --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index b23ccdc..ab757ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ [workspace] members = ["crates/ash_cli", "crates/ash_sdk"] +resolver = "2" [workspace.package] version = "0.4.1-rc.1" From 73b9083591b84a2a7dd82a423cd516e47148144e Mon Sep 17 00:00:00 2001 From: Gauthier Leonard Date: Tue, 13 Feb 2024 15:02:22 +0100 Subject: [PATCH 4/4] feat(sdk): add signer field to AvalancheSubnetValidator --- crates/ash_cli/src/utils/templating.rs | 11 +++++++++++ crates/ash_sdk/src/avalanche/subnets.rs | 3 +++ 2 files changed, 14 insertions(+) diff --git a/crates/ash_cli/src/utils/templating.rs b/crates/ash_cli/src/utils/templating.rs index 0558b1f..a772370 100644 --- a/crates/ash_cli/src/utils/templating.rs +++ b/crates/ash_cli/src/utils/templating.rs @@ -160,6 +160,9 @@ pub(crate) fn template_validator_info( let elastic_subnet_info = &formatdoc!( " Connected: {} + Signer (BLS): + Public key: {} + PoP: {} Uptime: {} Stake amount: {} Potential reward: {} @@ -175,6 +178,14 @@ pub(crate) fn template_validator_info( Threshold: {} Addresses: {}", type_colorize(&validator.connected), + type_colorize(&match validator.signer { + Some(ref signer) => format!("0x{}", hex::encode(signer.public_key.clone())), + None => String::from("None"), + }), + type_colorize(&match validator.signer { + Some(ref signer) => format!("0x{}", hex::encode(signer.proof_of_possession.clone())), + None => String::from("None"), + }), type_colorize(&validator.uptime.unwrap_or_default()), type_colorize(&validator.stake_amount.unwrap_or_default()), type_colorize(&validator.potential_reward.unwrap_or_default()), diff --git a/crates/ash_sdk/src/avalanche/subnets.rs b/crates/ash_sdk/src/avalanche/subnets.rs index eac5ca2..893ef0c 100644 --- a/crates/ash_sdk/src/avalanche/subnets.rs +++ b/crates/ash_sdk/src/avalanche/subnets.rs @@ -364,6 +364,8 @@ pub struct AvalancheSubnetValidator { pub potential_reward: Option, pub connected: bool, #[serde(skip_serializing_if = "Option::is_none")] + pub signer: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub uptime: Option, #[serde(skip_serializing_if = "Option::is_none")] pub validation_reward_owner: Option, @@ -391,6 +393,7 @@ impl AvalancheSubnetValidator { weight: validator.weight, potential_reward: validator.potential_reward, connected: validator.connected, + signer: validator.signer.clone(), uptime: validator.uptime, validation_reward_owner: validator .validation_reward_owner