From 4ef0fcf232f5f1cbe187a4e10b858e95ef88a6c3 Mon Sep 17 00:00:00 2001 From: mrnkslv Date: Wed, 8 Apr 2026 13:29:54 +0300 Subject: [PATCH 1/5] fix: max factor load from config --- src/node-control/README.md | 6 +-- .../commands/nodectl/config_elections_cmd.rs | 22 +++++++-- .../src/commands/nodectl/config_wallet_cmd.rs | 21 +++++++-- src/node-control/common/src/app_config.rs | 47 +++++++++++++++++-- src/node-control/common/src/ton_utils.rs | 9 ++++ src/node-control/elections/src/runner.rs | 30 ++++++++---- .../elections/src/runner_tests.rs | 6 +-- .../service/src/runtime_config.rs | 18 +++++++ .../src/v2/client_json_rpc.rs | 14 ++++++ 9 files changed, 145 insertions(+), 28 deletions(-) diff --git a/src/node-control/README.md b/src/node-control/README.md index 364d127..c596b8c 100644 --- a/src/node-control/README.md +++ b/src/node-control/README.md @@ -518,7 +518,7 @@ nodectl config elections tick-interval 60 ##### `config elections max-factor` -Set the maximum factor for elections. Must be in the range [1.0..3.0]. +Set the maximum stake factor for elections. The value must be between **1.0** and the network’s **maximum stake factor** from masterchain **config param 17** (`max_stake_factor`). nodectl does not use a hardcoded upper bound (e.g. 3.0): the CLI reads the current limit from the chain when validating and saving. | Argument | Description | |----------|-------------| @@ -1403,7 +1403,7 @@ Automatic elections task configuration: - `"minimum"` — use minimum required stake - `{ "fixed": }` — fixed stake amount in nanoTON - `policy_overrides` — per-node stake policy overrides (node name -> policy). When a node has an entry here, it takes precedence over the default `policy`. Example: `{ "node0": { "fixed": 500000000000 } }` -- `max_factor` — max factor for elections (default: 3.0, must be in range [1.0..3.0]) +- `max_factor` — maximum stake factor (default `3.0` in generated configs). Valid values lie in `[1.0, network_max]`, where **`network_max` comes from masterchain config param 17** (`max_stake_factor`); the CLI and stake command validate against the live network when TON HTTP API is available - `tick_interval` — interval between election checks in seconds (default: `40`) #### `voting` (optional) @@ -1720,7 +1720,7 @@ nodectl config wallet stake -b -a [-m ] |------|------|----------|---------|-------------| | `-b` | `--binding` | Yes | — | Binding name (node-wallet-pool triple) | | `-a` | `--amount` | Yes | — | Stake amount in TON | -| `-m` | `--max-factor` | No | `3.0` | Max factor (`1.0`–`3.0`) | +| `-m` | `--max-factor` | No | `3.0` | Max factor: from `1.0` up to the network limit (**config param 17**), validated against the chain | Example: diff --git a/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs b/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs index 4dae1cd..49d355d 100644 --- a/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs +++ b/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs @@ -6,7 +6,11 @@ * * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ -use crate::commands::nodectl::{output_format::OutputFormat, utils::save_config}; +use crate::commands::nodectl::{ + output_format::OutputFormat, + utils::{load_config_vault_rpc_client, save_config}, +}; +use anyhow::Context; use colored::Colorize; use common::{ app_config::{AppConfig, BindingStatus, ElectionsConfig, StakePolicy}, @@ -71,7 +75,9 @@ pub struct TickIntervalCmd { #[derive(clap::Args, Clone)] pub struct MaxFactorCmd { - #[arg(help = "Max factor (1.0..3.0)")] + #[arg( + help = "Max factor: from 1.0 up to the network limit (config param 17 max_stake_factor)" + )] value: f32, } @@ -220,8 +226,16 @@ impl TickIntervalCmd { impl MaxFactorCmd { pub async fn run(&self, path: &Path) -> anyhow::Result<()> { - if !(1.0..=3.0).contains(&self.value) { - anyhow::bail!("max-factor must be in range [1.0..3.0]"); + let (_app, _vault, rpc_client) = load_config_vault_rpc_client(path).await?; + let network_max = rpc_client + .network_max_stake_factor_multiplier() + .await + .context("read max_stake_factor from chain (config param 17)")?; + if !(1.0..=network_max).contains(&self.value) { + anyhow::bail!( + "max-factor must be in range [1.0..{}] (network max_stake_factor from config param 17)", + network_max + ); } let mut config = AppConfig::load(path)?; config diff --git a/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs b/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs index 2d57bc2..5619613 100644 --- a/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs +++ b/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs @@ -120,7 +120,12 @@ pub struct WalletStakeCmd { binding: String, #[arg(short = 'a', long = "amount", help = "Stake amount in TONs")] amount: f64, - #[arg(short = 'm', long = "max-factor", default_value = "3.0", help = "Max factor (1.0..3.0)")] + #[arg( + short = 'm', + long = "max-factor", + default_value = "3.0", + help = "Max factor from 1.0 up to the network limit (config param 17)" + )] max_factor: f32, } @@ -430,11 +435,17 @@ impl WalletSendCmd { impl WalletStakeCmd { pub async fn run(&self, path: &Path, cancellation_ctx: CancellationCtx) -> anyhow::Result<()> { - if !(1.0..=3.0).contains(&self.max_factor) { - anyhow::bail!("max-factor must be between 1.0 and 3.0"); - } - let (config, vault, rpc_client) = load_config_vault_rpc_client(path).await?; + let network_max = rpc_client + .network_max_stake_factor_multiplier() + .await + .context("read max_stake_factor from chain (config param 17)")?; + if !(1.0..=network_max).contains(&self.max_factor) { + anyhow::bail!( + "max-factor must be in range [1.0..{}] (network max_stake_factor from config param 17)", + network_max + ); + } // Resolve binding → wallet, pool, node let binding = config diff --git a/src/node-control/common/src/app_config.rs b/src/node-control/common/src/app_config.rs index 21d38ba..080db4c 100644 --- a/src/node-control/common/src/app_config.rs +++ b/src/node-control/common/src/app_config.rs @@ -459,7 +459,7 @@ fn default_workchain() -> i32 { -1 } -fn default_max_factor() -> f32 { +pub fn default_max_factor() -> f32 { 3.0 } @@ -506,10 +506,28 @@ impl ElectionsConfig { self.policy_overrides.get(node_id).unwrap_or(&self.policy) } - pub fn validate(&self) -> anyhow::Result<()> { - if !(1.0..=3.0).contains(&self.max_factor) { - anyhow::bail!("max_factor must be in range [1.0..3.0]"); + /// Validates elections settings. + /// + /// - `None`: only checks `max_factor >= 1.0` (e.g. [`AppConfig::load`] without RPC). No upper bound. + /// - `Some(m)`: `max_factor` must be in `[1.0, m]` where `m` is from config param 17 (service startup). + pub fn validate(&self, max_stake_factor_upper_bound: Option) -> anyhow::Result<()> { + self.validate_timing_fields()?; + match max_stake_factor_upper_bound { + None => { + if self.max_factor < 1.0 { + anyhow::bail!("max_factor must be >= 1.0"); + } + } + Some(m) => { + if !(1.0..=m).contains(&self.max_factor) { + anyhow::bail!("max_factor must be in range [1.0..{}]", m); + } + } } + Ok(()) + } + + fn validate_timing_fields(&self) -> anyhow::Result<()> { if !(0.0..=1.0).contains(&self.sleep_period_pct) { anyhow::bail!("sleep_period_pct must be in range [0.0..1.0]"); } @@ -714,7 +732,7 @@ impl AppConfig { } fn validate(&self) -> anyhow::Result<()> { - self.elections.as_ref().map(|e| e.validate()).transpose()?; + self.elections.as_ref().map(|e| e.validate(None)).transpose()?; Ok(()) } } @@ -756,6 +774,25 @@ mod tests { assert_eq!(stake, 10); } + #[test] + fn test_elections_validate_max_factor_respects_network_cap() { + let mut c = ElectionsConfig::default(); + c.max_factor = 5.0; + assert!(c.validate(Some(default_max_factor())).is_err()); + assert!(c.validate(Some(5.0)).is_ok()); + c.max_factor = 2.0; + assert!(c.validate(Some(default_max_factor())).is_ok()); + } + + #[test] + fn test_elections_validate_none_allows_max_factor_above_default_cap() { + let mut c = ElectionsConfig::default(); + c.max_factor = 25.0; + assert!(c.validate(None).is_ok()); + assert!(c.validate(Some(3.0)).is_err()); + assert!(c.validate(Some(30.0)).is_ok()); + } + #[test] fn test_calculate_stake_split50_ok() { let policy = StakePolicy::Split50; diff --git a/src/node-control/common/src/ton_utils.rs b/src/node-control/common/src/ton_utils.rs index 8837072..ebff8c8 100644 --- a/src/node-control/common/src/ton_utils.rs +++ b/src/node-control/common/src/ton_utils.rs @@ -18,6 +18,15 @@ pub fn nanotons_to_tons_f64(nanotons: u64) -> f64 { nanotons as f64 / 1_000_000_000.0 } +/// Elector uses fixed-point `max_stake_factor`: raw value is multiplier × 65536 (e.g. 3× → `3 * 65536`). +pub const MAX_STAKE_FACTOR_SCALE: u32 = 65536; + +/// Converts chain `max_stake_factor` (raw) to float multiplier (e.g. `196608` → `3.0`). +#[inline] +pub fn max_stake_factor_raw_to_multiplier(raw: u32) -> f32 { + raw as f32 / MAX_STAKE_FACTOR_SCALE as f32 +} + pub fn display_tons(nanotons: u64) -> String { format!("{:.4}", nanotons_to_tons_f64(nanotons)) .trim_end_matches('0') diff --git a/src/node-control/elections/src/runner.rs b/src/node-control/elections/src/runner.rs index a8394a4..e57d1a0 100644 --- a/src/node-control/elections/src/runner.rs +++ b/src/node-control/elections/src/runner.rs @@ -549,7 +549,8 @@ impl ElectionRunner { election_id: u64, params: &ConfigParams<'_>, ) -> anyhow::Result<()> { - let max_factor = (self.calc_max_factor() * 65536.0) as u32; + let max_factor = ((self.calc_max_factor() * 65536.0) as u32) + .clamp(common::ton_utils::MAX_STAKE_FACTOR_SCALE, params.cfg17.max_stake_factor); let stake_ctx = StakeContext { past_elections: &self.past_elections, our_max_factor: max_factor, @@ -691,7 +692,7 @@ impl ElectionRunner { max_factor, }); node.key_id = key_id; - Self::send_stake(node_id, node, stake).await?; + Self::send_stake(node_id, node, stake, params.cfg17.max_stake_factor).await?; Ok(()) } Some(entry) => { @@ -717,7 +718,8 @@ impl ElectionRunner { nanotons_to_tons_f64(old_stake + stake), nanotons_to_tons_f64(stake), ); - Self::send_stake(node_id, node, stake).await?; + Self::send_stake(node_id, node, stake, params.cfg17.max_stake_factor) + .await?; node.participant.as_mut().map(|p| p.stake += stake); } } @@ -726,7 +728,8 @@ impl ElectionRunner { if let Some(p) = node.participant.as_mut() { p.stake = stake; } - Self::send_stake(node_id, node, stake).await?; + Self::send_stake(node_id, node, stake, params.cfg17.max_stake_factor) + .await?; } } Ok(()) @@ -734,9 +737,15 @@ impl ElectionRunner { } } - async fn send_stake(node_id: &str, node: &mut Node, stake: u64) -> anyhow::Result<()> { + async fn send_stake( + node_id: &str, + node: &mut Node, + stake: u64, + network_max_stake_factor: u32, + ) -> anyhow::Result<()> { tracing::info!("node [{}] build stake message", node_id); - let payload = Self::build_new_stake_payload(node_id, node, stake).await?; + let payload = + Self::build_new_stake_payload(node_id, node, stake, network_max_stake_factor).await?; // For simplicity we always assume that the node has nominator pool. let fee = ELECTOR_STAKE_FEE + NPOOL_COMPUTE_FEE; let stake_balance = node.stake_balance(fee).await?; @@ -782,6 +791,7 @@ impl ElectionRunner { node_id: &str, node: &mut Node, stake: u64, + network_max_stake_factor: u32, ) -> anyhow::Result { let Some(participant) = &mut node.participant else { anyhow::bail!("node [{}] no participant info", node_id); @@ -798,8 +808,12 @@ impl ElectionRunner { participant.adnl_addr.as_slice() ) ); - if !(1.0..=3.0).contains(&(participant.max_factor as f32 / 65536.0)) { - anyhow::bail!(" must be a real number 1..3"); + let scale = common::ton_utils::MAX_STAKE_FACTOR_SCALE; + if participant.max_factor < scale || participant.max_factor > network_max_stake_factor { + anyhow::bail!( + " must be between 1.0 and {} (network max_stake_factor from config param 17)", + common::ton_utils::max_stake_factor_raw_to_multiplier(network_max_stake_factor) + ); } // todo: move to ElectorWrapper // validator-elect-req.fif diff --git a/src/node-control/elections/src/runner_tests.rs b/src/node-control/elections/src/runner_tests.rs index 702534e..13bd9d7 100644 --- a/src/node-control/elections/src/runner_tests.rs +++ b/src/node-control/elections/src/runner_tests.rs @@ -2382,7 +2382,7 @@ fn test_elections_config_validate_sleep_gt_waiting() { waiting_period_pct: 0.3, // sleep > waiting → invalid ..ElectionsConfig::default() }; - assert!(config.validate().is_err()); + assert!(config.validate(None).is_err()); } #[test] @@ -2391,7 +2391,7 @@ fn test_elections_config_validate_sleep_out_of_range() { sleep_period_pct: 1.5, // > 1.0 → invalid ..ElectionsConfig::default() }; - assert!(config.validate().is_err()); + assert!(config.validate(None).is_err()); } #[test] @@ -2401,7 +2401,7 @@ fn test_elections_config_validate_valid() { waiting_period_pct: 0.5, ..ElectionsConfig::default() }; - assert!(config.validate().is_ok()); + assert!(config.validate(None).is_ok()); } #[test] diff --git a/src/node-control/service/src/runtime_config.rs b/src/node-control/service/src/runtime_config.rs index 45be384..1f77613 100644 --- a/src/node-control/service/src/runtime_config.rs +++ b/src/node-control/service/src/runtime_config.rs @@ -93,6 +93,15 @@ impl RuntimeConfigStore { let vault = Some(SecretVaultBuilder::from_env().await?); let rpc_client = Self::load_rpc_client(&app_cfg).await?; + if let Some(elections) = app_cfg.elections.as_ref() { + let network_max = rpc_client + .network_max_stake_factor_multiplier() + .await + .context("read max_stake_factor for elections config validation")?; + elections + .validate(Some(network_max)) + .context("elections max_factor vs chain (config param 17)")?; + } let master_wallet = Self::load_master_wallet(&app_cfg, rpc_client.clone(), vault.clone()).await?; let wallets = Self::load_wallets(&app_cfg, rpc_client.clone(), vault.clone()).await?; @@ -116,6 +125,15 @@ impl RuntimeConfigStore { async fn reload(&self, new_config: AppConfig) -> anyhow::Result<()> { let vault = SecretVaultBuilder::from_env().await.context("failed to reopen vault")?; let rpc_client = Self::load_rpc_client(&new_config).await?; + if let Some(elections) = new_config.elections.as_ref() { + let network_max = rpc_client + .network_max_stake_factor_multiplier() + .await + .context("read max_stake_factor for elections config validation")?; + elections + .validate(Some(network_max)) + .context("elections max_factor vs chain (config param 17)")?; + } let master_wallet = Self::load_master_wallet(&new_config, rpc_client.clone(), Some(vault.clone())).await?; let wallets = diff --git a/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs b/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs index 246eb48..1031819 100644 --- a/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs +++ b/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs @@ -177,6 +177,20 @@ impl ClientJsonRpc { Ok(config_param) } + /// Global `max_stake_factor` from config param 17 (raw fixed-point, multiplier ×65536). + pub async fn network_max_stake_factor_raw(&self) -> anyhow::Result { + match self.get_config_param(17).await? { + ConfigParamEnum::ConfigParam17(c) => Ok(c.max_stake_factor), + _ => anyhow::bail!("expected config param 17 (stakes config)"), + } + } + + /// Same as [`Self::network_max_stake_factor_raw`], as float multiplier (e.g. `3.0`). + pub async fn network_max_stake_factor_multiplier(&self) -> anyhow::Result { + let raw = self.network_max_stake_factor_raw().await?; + Ok(common::ton_utils::max_stake_factor_raw_to_multiplier(raw)) + } + pub async fn run_get_method( &self, args: &RunGetMethodParams, From 60b598d555158b41dc9cb1396edba084e482bf59 Mon Sep 17 00:00:00 2001 From: mrnkslv Date: Wed, 8 Apr 2026 18:50:39 +0300 Subject: [PATCH 2/5] fix: default factor func not pub --- src/node-control/common/src/app_config.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node-control/common/src/app_config.rs b/src/node-control/common/src/app_config.rs index 080db4c..b9f933b 100644 --- a/src/node-control/common/src/app_config.rs +++ b/src/node-control/common/src/app_config.rs @@ -459,7 +459,7 @@ fn default_workchain() -> i32 { -1 } -pub fn default_max_factor() -> f32 { +fn default_max_factor() -> f32 { 3.0 } @@ -778,10 +778,10 @@ mod tests { fn test_elections_validate_max_factor_respects_network_cap() { let mut c = ElectionsConfig::default(); c.max_factor = 5.0; - assert!(c.validate(Some(default_max_factor())).is_err()); + assert!(c.validate(None).is_err()); assert!(c.validate(Some(5.0)).is_ok()); c.max_factor = 2.0; - assert!(c.validate(Some(default_max_factor())).is_ok()); + assert!(c.validate(None).is_ok()); } #[test] From 28b7178e47d9190acbe202086ae57db7151b8558 Mon Sep 17 00:00:00 2001 From: mrnkslv Date: Wed, 8 Apr 2026 19:02:55 +0300 Subject: [PATCH 3/5] fix: fix test --- src/node-control/common/src/app_config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node-control/common/src/app_config.rs b/src/node-control/common/src/app_config.rs index b9f933b..5b9ab86 100644 --- a/src/node-control/common/src/app_config.rs +++ b/src/node-control/common/src/app_config.rs @@ -778,10 +778,10 @@ mod tests { fn test_elections_validate_max_factor_respects_network_cap() { let mut c = ElectionsConfig::default(); c.max_factor = 5.0; - assert!(c.validate(None).is_err()); + assert!(c.validate(Some(default_max_factor())).is_err()); assert!(c.validate(Some(5.0)).is_ok()); c.max_factor = 2.0; - assert!(c.validate(None).is_ok()); + assert!(c.validate(Some(default_max_factor())).is_ok()); } #[test] From ea9ff35bb4e9f04caa3c3abae1c8c5d816c407c4 Mon Sep 17 00:00:00 2001 From: mrnkslv Date: Thu, 9 Apr 2026 14:05:01 +0300 Subject: [PATCH 4/5] fix: corrections according to comments --- src/node-control/README.md | 2 +- .../commands/nodectl/config_elections_cmd.rs | 27 ++++--- .../src/commands/nodectl/config_wallet_cmd.rs | 17 ++--- .../commands/src/commands/nodectl/utils.rs | 5 ++ src/node-control/common/src/app_config.rs | 18 ++--- src/node-control/common/src/ton_utils.rs | 14 +++- src/node-control/elections/src/runner.rs | 72 +++++++++++-------- .../elections/src/runner_tests.rs | 2 + .../service/src/runtime_config.rs | 39 ++++++---- .../src/v2/client_json_rpc.rs | 14 ---- 10 files changed, 111 insertions(+), 99 deletions(-) diff --git a/src/node-control/README.md b/src/node-control/README.md index c596b8c..b3c697f 100644 --- a/src/node-control/README.md +++ b/src/node-control/README.md @@ -1403,7 +1403,7 @@ Automatic elections task configuration: - `"minimum"` — use minimum required stake - `{ "fixed": }` — fixed stake amount in nanoTON - `policy_overrides` — per-node stake policy overrides (node name -> policy). When a node has an entry here, it takes precedence over the default `policy`. Example: `{ "node0": { "fixed": 500000000000 } }` -- `max_factor` — maximum stake factor (default `3.0` in generated configs). Valid values lie in `[1.0, network_max]`, where **`network_max` comes from masterchain config param 17** (`max_stake_factor`); the CLI and stake command validate against the live network when TON HTTP API is available +- `max_factor` — maximum stake factor (default `3.0` in generated configs). Valid values lie in `[1.0, network_max_factor]`, where **`network_max_factor` comes from masterchain config param 17** (`max_stake_factor`); the CLI and stake command validate against the live network when TON HTTP API is available - `tick_interval` — interval between election checks in seconds (default: `40`) #### `voting` (optional) diff --git a/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs b/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs index 49d355d..2d5c042 100644 --- a/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs +++ b/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs @@ -8,9 +8,8 @@ */ use crate::commands::nodectl::{ output_format::OutputFormat, - utils::{load_config_vault_rpc_client, save_config}, + utils::{fetch_network_max_factor, save_config, try_create_rpc_client}, }; -use anyhow::Context; use colored::Colorize; use common::{ app_config::{AppConfig, BindingStatus, ElectionsConfig, StakePolicy}, @@ -226,23 +225,21 @@ impl TickIntervalCmd { impl MaxFactorCmd { pub async fn run(&self, path: &Path) -> anyhow::Result<()> { - let (_app, _vault, rpc_client) = load_config_vault_rpc_client(path).await?; - let network_max = rpc_client - .network_max_stake_factor_multiplier() - .await - .context("read max_stake_factor from chain (config param 17)")?; - if !(1.0..=network_max).contains(&self.value) { + let mut config = AppConfig::load(path)?; + if config.elections.is_none() { + anyhow::bail!("Elections are not configured"); + } + + let rpc_client = try_create_rpc_client(&config).await?; + let network_max_factor = fetch_network_max_factor(&rpc_client).await?; + if !(1.0..=network_max_factor).contains(&self.value) { anyhow::bail!( "max-factor must be in range [1.0..{}] (network max_stake_factor from config param 17)", - network_max + network_max_factor ); } - let mut config = AppConfig::load(path)?; - config - .elections - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Elections are not configured"))? - .max_factor = self.value; + + config.elections.as_mut().unwrap().max_factor = self.value; save_config(&config, path)?; println!("{} Max factor set to {}", "OK".green().bold(), self.value); Ok(()) diff --git a/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs b/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs index fd2f011..a9bd6e6 100644 --- a/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs +++ b/src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs @@ -9,10 +9,10 @@ use crate::commands::nodectl::{ output_format::OutputFormat, utils::{ - MASTER_WALLET_RESERVED_NAME, SEND_TIMEOUT, check_ton_api_connection, get_wallet_config, - load_config_vault, load_config_vault_rpc_client, make_wallet, save_config, - wait_for_seqno_change, wallet_address, wallet_info, warn_missing_secret, - warn_ton_api_unavailable, + MASTER_WALLET_RESERVED_NAME, SEND_TIMEOUT, check_ton_api_connection, + fetch_network_max_factor, get_wallet_config, load_config_vault, + load_config_vault_rpc_client, make_wallet, save_config, wait_for_seqno_change, + wallet_address, wallet_info, warn_missing_secret, warn_ton_api_unavailable, }, }; use anyhow::Context; @@ -441,14 +441,11 @@ impl WalletSendCmd { impl WalletStakeCmd { pub async fn run(&self, path: &Path, cancellation_ctx: CancellationCtx) -> anyhow::Result<()> { let (config, vault, rpc_client) = load_config_vault_rpc_client(path).await?; - let network_max = rpc_client - .network_max_stake_factor_multiplier() - .await - .context("read max_stake_factor from chain (config param 17)")?; - if !(1.0..=network_max).contains(&self.max_factor) { + let network_max_factor = fetch_network_max_factor(&rpc_client).await?; + if !(1.0..=network_max_factor).contains(&self.max_factor) { anyhow::bail!( "max-factor must be in range [1.0..{}] (network max_stake_factor from config param 17)", - network_max + network_max_factor ); } diff --git a/src/node-control/commands/src/commands/nodectl/utils.rs b/src/node-control/commands/src/commands/nodectl/utils.rs index df78305..1508432 100644 --- a/src/node-control/commands/src/commands/nodectl/utils.rs +++ b/src/node-control/commands/src/commands/nodectl/utils.rs @@ -32,6 +32,11 @@ pub const DEPLOY_TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_se /// Logical name for the master wallet in CLI, `get_wallet_config`, and `config wallet ls`. pub const MASTER_WALLET_RESERVED_NAME: &str = "master_wallet"; +/// `max_stake_factor` from masterchain config param 17 as a float multiplier (e.g. `3.0`). +pub async fn fetch_network_max_factor(rpc_client: &ClientJsonRpc) -> anyhow::Result { + common::ton_utils::extract_max_stake_factor(rpc_client.get_config_param(17).await?) +} + pub fn warn_missing_secret(secret_name: &str) { println!("\n{} {}", "[WARNING]".yellow().bold(), "Vault secret is missing".yellow(),); println!( diff --git a/src/node-control/common/src/app_config.rs b/src/node-control/common/src/app_config.rs index 5b9ab86..6351083 100644 --- a/src/node-control/common/src/app_config.rs +++ b/src/node-control/common/src/app_config.rs @@ -510,18 +510,14 @@ impl ElectionsConfig { /// /// - `None`: only checks `max_factor >= 1.0` (e.g. [`AppConfig::load`] without RPC). No upper bound. /// - `Some(m)`: `max_factor` must be in `[1.0, m]` where `m` is from config param 17 (service startup). - pub fn validate(&self, max_stake_factor_upper_bound: Option) -> anyhow::Result<()> { + pub fn validate(&self, max_factor_upper_bound: Option) -> anyhow::Result<()> { self.validate_timing_fields()?; - match max_stake_factor_upper_bound { - None => { - if self.max_factor < 1.0 { - anyhow::bail!("max_factor must be >= 1.0"); - } - } - Some(m) => { - if !(1.0..=m).contains(&self.max_factor) { - anyhow::bail!("max_factor must be in range [1.0..{}]", m); - } + if self.max_factor < 1.0 { + anyhow::bail!("max_factor must be >= 1.0"); + } + if let Some(m) = max_factor_upper_bound { + if self.max_factor > m { + anyhow::bail!("max_factor must be in range [1.0..{}]", m); } } Ok(()) diff --git a/src/node-control/common/src/ton_utils.rs b/src/node-control/common/src/ton_utils.rs index ebff8c8..b3787aa 100644 --- a/src/node-control/common/src/ton_utils.rs +++ b/src/node-control/common/src/ton_utils.rs @@ -19,12 +19,22 @@ pub fn nanotons_to_tons_f64(nanotons: u64) -> f64 { } /// Elector uses fixed-point `max_stake_factor`: raw value is multiplier × 65536 (e.g. 3× → `3 * 65536`). -pub const MAX_STAKE_FACTOR_SCALE: u32 = 65536; +pub const MAX_STAKE_FACTOR_SCALE: f32 = 65536.0; /// Converts chain `max_stake_factor` (raw) to float multiplier (e.g. `196608` → `3.0`). #[inline] pub fn max_stake_factor_raw_to_multiplier(raw: u32) -> f32 { - raw as f32 / MAX_STAKE_FACTOR_SCALE as f32 + raw as f32 / MAX_STAKE_FACTOR_SCALE +} + +/// Extracts `max_stake_factor` from a `ConfigParamEnum` (must be param 17) as a float multiplier. +pub fn extract_max_stake_factor(param: ton_block::ConfigParamEnum) -> anyhow::Result { + match param { + ton_block::ConfigParamEnum::ConfigParam17(c) => { + Ok(max_stake_factor_raw_to_multiplier(c.max_stake_factor)) + } + _ => anyhow::bail!("expected config param 17 (stakes config)"), + } } pub fn display_tons(nanotons: u64) -> String { diff --git a/src/node-control/elections/src/runner.rs b/src/node-control/elections/src/runner.rs index e57d1a0..23eac30 100644 --- a/src/node-control/elections/src/runner.rs +++ b/src/node-control/elections/src/runner.rs @@ -21,7 +21,10 @@ use common::{ }, task_cancellation::CancellationCtx, time_format, - ton_utils::{display_tons, nanotons_to_dec_string, nanotons_to_tons_f64}, + ton_utils::{ + MAX_STAKE_FACTOR_SCALE, display_tons, max_stake_factor_raw_to_multiplier, + nanotons_to_dec_string, nanotons_to_tons_f64, + }, }; use contracts::{ ElectionsInfo, ElectorWrapper, NominatorWrapper, Participant, TonWallet, @@ -391,7 +394,11 @@ impl ElectionRunner { elections_info.participants.len() ); - self.build_elections_snapshot(election_id, &cfg15, &elections_info); + // Config param 17: effective `max_factor` in snapshot; 16/17: participation (e.g. AdaptiveSplit50). + let cfg16 = self.fetch_config_param_16().await?; + let cfg17 = self.fetch_config_param_17().await?; + + self.build_elections_snapshot(election_id, &cfg15, &elections_info, &cfg17); if elections_info.finished { self.snapshot_cache.last_elections_status = ElectionsStatus::Finished; @@ -436,9 +443,6 @@ impl ElectionRunner { ); } } - // Fetch config params 16/17 - used for AdaptiveSplit50 strategy - let cfg16 = self.fetch_config_param_16().await?; - let cfg17 = self.fetch_config_param_17().await?; // walk through the nodes and try to participate in the elections let mut nodes = self.nodes.keys().cloned().collect::>(); @@ -491,8 +495,9 @@ impl ElectionRunner { election_id: u64, cfg15: &ConfigParam15, elections_info: &ElectionsInfo, + cfg17: &ConfigParam17, ) { - self.snapshot_cache.last_max_factor = Some(self.calc_max_factor()); + self.snapshot_cache.last_max_factor = Some(self.calc_max_factor(cfg17.max_stake_factor)); // It can be a validator wallet or nominator pool address. let wallet_addrs: HashSet> = @@ -549,8 +554,15 @@ impl ElectionRunner { election_id: u64, params: &ConfigParams<'_>, ) -> anyhow::Result<()> { - let max_factor = ((self.calc_max_factor() * 65536.0) as u32) - .clamp(common::ton_utils::MAX_STAKE_FACTOR_SCALE, params.cfg17.max_stake_factor); + let configured_raw = self.configured_max_factor_raw(); + let max_factor = self.calc_max_factor_raw(params.cfg17.max_stake_factor); + if max_factor != configured_raw { + tracing::warn!( + "max_factor clamped: configured={}, used={} (network limit from cfg17)", + max_stake_factor_raw_to_multiplier(configured_raw), + max_stake_factor_raw_to_multiplier(max_factor), + ); + } let stake_ctx = StakeContext { past_elections: &self.past_elections, our_max_factor: max_factor, @@ -692,7 +704,7 @@ impl ElectionRunner { max_factor, }); node.key_id = key_id; - Self::send_stake(node_id, node, stake, params.cfg17.max_stake_factor).await?; + Self::send_stake(node_id, node, stake).await?; Ok(()) } Some(entry) => { @@ -718,8 +730,7 @@ impl ElectionRunner { nanotons_to_tons_f64(old_stake + stake), nanotons_to_tons_f64(stake), ); - Self::send_stake(node_id, node, stake, params.cfg17.max_stake_factor) - .await?; + Self::send_stake(node_id, node, stake).await?; node.participant.as_mut().map(|p| p.stake += stake); } } @@ -728,8 +739,7 @@ impl ElectionRunner { if let Some(p) = node.participant.as_mut() { p.stake = stake; } - Self::send_stake(node_id, node, stake, params.cfg17.max_stake_factor) - .await?; + Self::send_stake(node_id, node, stake).await?; } } Ok(()) @@ -737,15 +747,9 @@ impl ElectionRunner { } } - async fn send_stake( - node_id: &str, - node: &mut Node, - stake: u64, - network_max_stake_factor: u32, - ) -> anyhow::Result<()> { + async fn send_stake(node_id: &str, node: &mut Node, stake: u64) -> anyhow::Result<()> { tracing::info!("node [{}] build stake message", node_id); - let payload = - Self::build_new_stake_payload(node_id, node, stake, network_max_stake_factor).await?; + let payload = Self::build_new_stake_payload(node_id, node, stake).await?; // For simplicity we always assume that the node has nominator pool. let fee = ELECTOR_STAKE_FEE + NPOOL_COMPUTE_FEE; let stake_balance = node.stake_balance(fee).await?; @@ -791,7 +795,6 @@ impl ElectionRunner { node_id: &str, node: &mut Node, stake: u64, - network_max_stake_factor: u32, ) -> anyhow::Result { let Some(participant) = &mut node.participant else { anyhow::bail!("node [{}] no participant info", node_id); @@ -808,13 +811,6 @@ impl ElectionRunner { participant.adnl_addr.as_slice() ) ); - let scale = common::ton_utils::MAX_STAKE_FACTOR_SCALE; - if participant.max_factor < scale || participant.max_factor > network_max_stake_factor { - anyhow::bail!( - " must be between 1.0 and {} (network max_stake_factor from config param 17)", - common::ton_utils::max_stake_factor_raw_to_multiplier(network_max_stake_factor) - ); - } // todo: move to ElectorWrapper // validator-elect-req.fif let mut data = 0x654C5074u32.to_be_bytes().to_vec(); @@ -937,8 +933,22 @@ impl ElectionRunner { tracing::info!("elections: start={}, end={}", elections_start, elections_end); } - fn calc_max_factor(&self) -> f32 { - self.default_max_factor + /// Effective stake multiplier after applying configured `default_max_factor` and chain cap + /// (`network_max_stake_factor_raw` from masterchain config param 17). + fn calc_max_factor(&self, network_max_stake_factor_raw: u32) -> f32 { + max_stake_factor_raw_to_multiplier(self.calc_max_factor_raw(network_max_stake_factor_raw)) + } + + #[inline] + fn configured_max_factor_raw(&self) -> u32 { + (self.default_max_factor * MAX_STAKE_FACTOR_SCALE) as u32 + } + + /// Elector `max_factor` fixed-point value: configured multiplier as raw, clamped to + /// `[65536, network_max_stake_factor_raw]` (fixed-point scale). + fn calc_max_factor_raw(&self, network_max_stake_factor_raw: u32) -> u32 { + self.configured_max_factor_raw() + .clamp(MAX_STAKE_FACTOR_SCALE as u32, network_max_stake_factor_raw) } /// Calculate stake for a node according to the stake policy. diff --git a/src/node-control/elections/src/runner_tests.rs b/src/node-control/elections/src/runner_tests.rs index 13bd9d7..b82edb1 100644 --- a/src/node-control/elections/src/runner_tests.rs +++ b/src/node-control/elections/src/runner_tests.rs @@ -1352,6 +1352,8 @@ async fn test_elections_finished_node_not_in_participants() { let provider = &mut harness.provider_mock; provider.expect_election_parameters().returning(|| Ok(default_cfg15())); provider.expect_validator_config().returning(|| Ok(ValidatorConfig::new())); + provider.expect_config_param_16().returning(|| Ok(default_cfg16())); + provider.expect_config_param_17().returning(|| Ok(default_cfg17())); provider.expect_shutdown().returning(|| Ok(())); diff --git a/src/node-control/service/src/runtime_config.rs b/src/node-control/service/src/runtime_config.rs index 1f77613..b69b263 100644 --- a/src/node-control/service/src/runtime_config.rs +++ b/src/node-control/service/src/runtime_config.rs @@ -8,7 +8,7 @@ */ use anyhow::Context; use common::{ - app_config::{AppConfig, KeyConfig, PoolConfig, WalletConfig}, + app_config::{AppConfig, ElectionsConfig, KeyConfig, PoolConfig, WalletConfig}, time_format, vault_signer::VaultSigner, }; @@ -94,13 +94,7 @@ impl RuntimeConfigStore { let vault = Some(SecretVaultBuilder::from_env().await?); let rpc_client = Self::load_rpc_client(&app_cfg).await?; if let Some(elections) = app_cfg.elections.as_ref() { - let network_max = rpc_client - .network_max_stake_factor_multiplier() - .await - .context("read max_stake_factor for elections config validation")?; - elections - .validate(Some(network_max)) - .context("elections max_factor vs chain (config param 17)")?; + Self::validate_elections_max_factor_vs_chain(&rpc_client, elections).await?; } let master_wallet = Self::load_master_wallet(&app_cfg, rpc_client.clone(), vault.clone()).await?; @@ -126,13 +120,7 @@ impl RuntimeConfigStore { let vault = SecretVaultBuilder::from_env().await.context("failed to reopen vault")?; let rpc_client = Self::load_rpc_client(&new_config).await?; if let Some(elections) = new_config.elections.as_ref() { - let network_max = rpc_client - .network_max_stake_factor_multiplier() - .await - .context("read max_stake_factor for elections config validation")?; - elections - .validate(Some(network_max)) - .context("elections max_factor vs chain (config param 17)")?; + Self::validate_elections_max_factor_vs_chain(&rpc_client, elections).await?; } let master_wallet = Self::load_master_wallet(&new_config, rpc_client.clone(), Some(vault.clone())).await?; @@ -153,6 +141,27 @@ impl RuntimeConfigStore { Ok(()) } + async fn validate_elections_max_factor_vs_chain( + rpc_client: &ClientJsonRpc, + elections: &ElectionsConfig, + ) -> anyhow::Result<()> { + match rpc_client + .get_config_param(17) + .await + .and_then(common::ton_utils::extract_max_stake_factor) + { + Ok(network_max_factor) => elections.validate(Some(network_max_factor)), + Err(e) => { + tracing::warn!( + error = %e, + "elections max_factor: failed to read config param 17 from chain; \ + validating without network upper bound (re-check max_factor when TON HTTP API is reachable)" + ); + elections.validate(None) + } + } + } + #[cfg(test)] pub fn from_app_config(app_config: Arc) -> Self { use contracts::SmartContract; diff --git a/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs b/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs index 1031819..246eb48 100644 --- a/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs +++ b/src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs @@ -177,20 +177,6 @@ impl ClientJsonRpc { Ok(config_param) } - /// Global `max_stake_factor` from config param 17 (raw fixed-point, multiplier ×65536). - pub async fn network_max_stake_factor_raw(&self) -> anyhow::Result { - match self.get_config_param(17).await? { - ConfigParamEnum::ConfigParam17(c) => Ok(c.max_stake_factor), - _ => anyhow::bail!("expected config param 17 (stakes config)"), - } - } - - /// Same as [`Self::network_max_stake_factor_raw`], as float multiplier (e.g. `3.0`). - pub async fn network_max_stake_factor_multiplier(&self) -> anyhow::Result { - let raw = self.network_max_stake_factor_raw().await?; - Ok(common::ton_utils::max_stake_factor_raw_to_multiplier(raw)) - } - pub async fn run_get_method( &self, args: &RunGetMethodParams, From ffde6211a2259e76628a150398ef0c7db14982c7 Mon Sep 17 00:00:00 2001 From: mrnkslv Date: Fri, 10 Apr 2026 02:46:31 +0300 Subject: [PATCH 5/5] fix: corrections according to comments --- .../commands/nodectl/config_elections_cmd.rs | 10 ++-- .../commands/src/commands/nodectl/utils.rs | 3 +- src/node-control/common/src/ton_utils.rs | 8 +-- src/node-control/elections/src/runner.rs | 51 +++++++++---------- .../service/src/runtime_config.rs | 7 +-- 5 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs b/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs index 2d5c042..29f2e3b 100644 --- a/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs +++ b/src/node-control/commands/src/commands/nodectl/config_elections_cmd.rs @@ -226,9 +226,7 @@ impl TickIntervalCmd { impl MaxFactorCmd { pub async fn run(&self, path: &Path) -> anyhow::Result<()> { let mut config = AppConfig::load(path)?; - if config.elections.is_none() { - anyhow::bail!("Elections are not configured"); - } + config.elections.as_ref().ok_or_else(|| anyhow::anyhow!("Elections are not configured"))?; let rpc_client = try_create_rpc_client(&config).await?; let network_max_factor = fetch_network_max_factor(&rpc_client).await?; @@ -239,7 +237,11 @@ impl MaxFactorCmd { ); } - config.elections.as_mut().unwrap().max_factor = self.value; + config + .elections + .as_mut() + .ok_or_else(|| anyhow::anyhow!("Elections are not configured"))? + .max_factor = self.value; save_config(&config, path)?; println!("{} Max factor set to {}", "OK".green().bold(), self.value); Ok(()) diff --git a/src/node-control/commands/src/commands/nodectl/utils.rs b/src/node-control/commands/src/commands/nodectl/utils.rs index 1508432..798195f 100644 --- a/src/node-control/commands/src/commands/nodectl/utils.rs +++ b/src/node-control/commands/src/commands/nodectl/utils.rs @@ -11,6 +11,7 @@ use colored::Colorize; use common::{ app_config::{AppConfig, WalletConfig}, task_cancellation::CancellationCtx, + ton_utils::extract_max_factor, vault_signer::VaultSigner, }; use contracts::{WalletContract, contract_provider}; @@ -34,7 +35,7 @@ pub const MASTER_WALLET_RESERVED_NAME: &str = "master_wallet"; /// `max_stake_factor` from masterchain config param 17 as a float multiplier (e.g. `3.0`). pub async fn fetch_network_max_factor(rpc_client: &ClientJsonRpc) -> anyhow::Result { - common::ton_utils::extract_max_stake_factor(rpc_client.get_config_param(17).await?) + extract_max_factor(rpc_client.get_config_param(17).await?) } pub fn warn_missing_secret(secret_name: &str) { diff --git a/src/node-control/common/src/ton_utils.rs b/src/node-control/common/src/ton_utils.rs index b3787aa..6291a36 100644 --- a/src/node-control/common/src/ton_utils.rs +++ b/src/node-control/common/src/ton_utils.rs @@ -6,6 +6,8 @@ * * This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. */ +use ton_block::ConfigParamEnum; + pub fn nanotons_to_dec_string(value: u64) -> String { value.to_string() } @@ -27,10 +29,10 @@ pub fn max_stake_factor_raw_to_multiplier(raw: u32) -> f32 { raw as f32 / MAX_STAKE_FACTOR_SCALE } -/// Extracts `max_stake_factor` from a `ConfigParamEnum` (must be param 17) as a float multiplier. -pub fn extract_max_stake_factor(param: ton_block::ConfigParamEnum) -> anyhow::Result { +/// Extracts the network `max_factor` from a `ConfigParamEnum` (must be param 17; field `max_stake_factor`) as a float multiplier. +pub fn extract_max_factor(param: ConfigParamEnum) -> anyhow::Result { match param { - ton_block::ConfigParamEnum::ConfigParam17(c) => { + ConfigParamEnum::ConfigParam17(c) => { Ok(max_stake_factor_raw_to_multiplier(c.max_stake_factor)) } _ => anyhow::bail!("expected config param 17 (stakes config)"), diff --git a/src/node-control/elections/src/runner.rs b/src/node-control/elections/src/runner.rs index 23eac30..0065d40 100644 --- a/src/node-control/elections/src/runner.rs +++ b/src/node-control/elections/src/runner.rs @@ -497,7 +497,8 @@ impl ElectionRunner { elections_info: &ElectionsInfo, cfg17: &ConfigParam17, ) { - self.snapshot_cache.last_max_factor = Some(self.calc_max_factor(cfg17.max_stake_factor)); + self.snapshot_cache.last_max_factor = + Some(self.calc_max_factor(cfg17.max_stake_factor, false).1); // It can be a validator wallet or nominator pool address. let wallet_addrs: HashSet> = @@ -554,15 +555,7 @@ impl ElectionRunner { election_id: u64, params: &ConfigParams<'_>, ) -> anyhow::Result<()> { - let configured_raw = self.configured_max_factor_raw(); - let max_factor = self.calc_max_factor_raw(params.cfg17.max_stake_factor); - if max_factor != configured_raw { - tracing::warn!( - "max_factor clamped: configured={}, used={} (network limit from cfg17)", - max_stake_factor_raw_to_multiplier(configured_raw), - max_stake_factor_raw_to_multiplier(max_factor), - ); - } + let (max_factor, _) = self.calc_max_factor(params.cfg17.max_stake_factor, true); let stake_ctx = StakeContext { past_elections: &self.past_elections, our_max_factor: max_factor, @@ -933,22 +926,28 @@ impl ElectionRunner { tracing::info!("elections: start={}, end={}", elections_start, elections_end); } - /// Effective stake multiplier after applying configured `default_max_factor` and chain cap - /// (`network_max_stake_factor_raw` from masterchain config param 17). - fn calc_max_factor(&self, network_max_stake_factor_raw: u32) -> f32 { - max_stake_factor_raw_to_multiplier(self.calc_max_factor_raw(network_max_stake_factor_raw)) - } - - #[inline] - fn configured_max_factor_raw(&self) -> u32 { - (self.default_max_factor * MAX_STAKE_FACTOR_SCALE) as u32 - } - - /// Elector `max_factor` fixed-point value: configured multiplier as raw, clamped to - /// `[65536, network_max_stake_factor_raw]` (fixed-point scale). - fn calc_max_factor_raw(&self, network_max_stake_factor_raw: u32) -> u32 { - self.configured_max_factor_raw() - .clamp(MAX_STAKE_FACTOR_SCALE as u32, network_max_stake_factor_raw) + /// Resolves elector `max_factor`: fixed-point `raw` for the Elector and `multiplier` for logs/UI. + /// + /// Applies configured `default_max_factor` and clamps to the chain cap + /// (`network_max_stake_factor_raw` from masterchain config param 17), in fixed-point + /// `[65536, network_max_stake_factor_raw]` (see [`MAX_STAKE_FACTOR_SCALE`]). + /// + /// When `warn_if_clamped` is true and the configured value was clamped, logs a warning. + fn calc_max_factor( + &self, + network_max_stake_factor_raw: u32, + warn_if_clamped: bool, + ) -> (u32, f32) { + let configured_raw = (self.default_max_factor * MAX_STAKE_FACTOR_SCALE) as u32; + let raw = configured_raw.clamp(MAX_STAKE_FACTOR_SCALE as u32, network_max_stake_factor_raw); + if warn_if_clamped && raw != configured_raw { + tracing::warn!( + "max_factor clamped: configured={}, used={} (network limit from cfg17)", + max_stake_factor_raw_to_multiplier(configured_raw), + max_stake_factor_raw_to_multiplier(raw), + ); + } + (raw, max_stake_factor_raw_to_multiplier(raw)) } /// Calculate stake for a node according to the stake policy. diff --git a/src/node-control/service/src/runtime_config.rs b/src/node-control/service/src/runtime_config.rs index b69b263..70d9358 100644 --- a/src/node-control/service/src/runtime_config.rs +++ b/src/node-control/service/src/runtime_config.rs @@ -10,6 +10,7 @@ use anyhow::Context; use common::{ app_config::{AppConfig, ElectionsConfig, KeyConfig, PoolConfig, WalletConfig}, time_format, + ton_utils::extract_max_factor, vault_signer::VaultSigner, }; use contracts::{ @@ -145,11 +146,7 @@ impl RuntimeConfigStore { rpc_client: &ClientJsonRpc, elections: &ElectionsConfig, ) -> anyhow::Result<()> { - match rpc_client - .get_config_param(17) - .await - .and_then(common::ton_utils::extract_max_stake_factor) - { + match rpc_client.get_config_param(17).await.and_then(extract_max_factor) { Ok(network_max_factor) => elections.validate(Some(network_max_factor)), Err(e) => { tracing::warn!(