Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
b163f09
Bump node version up for upcoming release
bvscd Mar 23, 2026
995188f
Validate query uses version and capabilities from config instead of b…
yaroslavser Mar 24, 2026
5707d8c
Merge branch 'master' into fix_use_config_in_validate
yaroslavser Mar 24, 2026
9cf68c5
Validate query uses capabilities from blockchain config, not candidat…
yaroslavser Mar 25, 2026
72f7bb6
Validate query uses capabilities from blockchain config, not candidat…
yaroslavser Mar 25, 2026
82644c4
Merge pull request #30 from RSquad/fix_use_config_in_validate
bvscd Mar 25, 2026
349ad01
Simplex fixes and upgrades
bvscd Mar 25, 2026
d3ce15a
Adjust node version
bvscd Mar 25, 2026
0947899
Fix for clip
bvscd Mar 25, 2026
ba1cce8
Merge pull request #33 from RSquad/simplex_upgrade
bvscd Mar 26, 2026
4bec033
CellsDB: cells cache
bvscd Mar 26, 2026
b470ac9
Merge branch 'release/node/v0.4.0' into cells_cache
bvscd Mar 26, 2026
d6ba78c
Merge pull request #34 from RSquad/cells_cache
bvscd Mar 26, 2026
d0e3928
Reliable fast sync overlay creation
bvscd Mar 26, 2026
4e841c2
Merge branch 'release/node/v0.4.0' into fix_overlays
bvscd Mar 26, 2026
4d3397f
Merge pull request #35 from RSquad/fix_overlays
bvscd Mar 26, 2026
8566cf7
Fix changed untouched account
yaroslavser Mar 30, 2026
4c7a30e
fix fmt
yaroslavser Mar 30, 2026
583500b
Merge pull request #38 from RSquad/fix-unchanged-account
bvscd Mar 31, 2026
1300299
Fix config params creation for emulator
yaroslavser Mar 31, 2026
de2717d
Fix config params creation for emulator
yaroslavser Mar 31, 2026
d3e7026
Fix config params creation for emulator
yaroslavser Mar 27, 2026
5e5471a
Latest simplex changes
bvscd Apr 1, 2026
2ad771d
Fix formatting
yaroslavser Mar 30, 2026
d954d0c
Merge pull request #42 from RSquad/simplex_upgrade
bvscd Apr 1, 2026
38a8a8b
Merge branch 'release/node/v0.4.0' into fix-config-for-emulator
yaroslavser Apr 1, 2026
f0454ee
Additional updates on Simplex
bvscd Apr 1, 2026
f8395b8
Merge pull request #44 from RSquad/simplex_upgrade
bvscd Apr 1, 2026
fa44887
Merge branch 'release/node/v0.4.0' into fix-config-for-emulator
bvscd Apr 1, 2026
a4409ca
Merge pull request #40 from RSquad/fix-config-for-emulator
bvscd Apr 1, 2026
8bd0d74
chore(helm): add quic and simplex logger targets
inyellowbus Apr 1, 2026
228770b
feat(helm): add terminationGracePeriodSeconds parameter (#45)
inyellowbus Apr 1, 2026
7f09052
Prevent sync by archives to be stalled when shards split/merge
bvscd Apr 1, 2026
fec4321
Merge pull request #46 from RSquad/fix_sync
bvscd Apr 1, 2026
93e41ea
Archival node functionality
bvscd Apr 1, 2026
24f56aa
fix(helm): fix invalid dnsPolicy and make it configurable
inyellowbus Apr 2, 2026
83a2d74
Fix storage phase for special accounts - pay for due
yaroslavser Apr 2, 2026
c9f19b8
Merge pull request #48 from RSquad/release/helm/node/v0.4.4
inyellowbus Apr 2, 2026
fadeb46
Merge pull request #47 from RSquad/archival_node
bvscd Apr 2, 2026
6cb5d3f
More changes on Simplex/QUIC
bvscd Apr 2, 2026
ef6e0d5
Merge pull request #51 from RSquad/simplex_upgrade
bvscd Apr 2, 2026
35b8421
Cancellation safety for AsyncReceiver
bvscd Apr 2, 2026
c5a43af
Merge pull request #52 from RSquad/fix_async
bvscd Apr 2, 2026
be2422b
ci(node): add node release workflow
inyellowbus Apr 2, 2026
007a5e3
ci(node): fix commit timestamp for tag push events
inyellowbus Apr 2, 2026
61d45db
Merge pull request #50 from RSquad/ci/node-release-workflow
bvscd Apr 2, 2026
837dd23
ci(node): use ton-large runner for container build
inyellowbus Apr 2, 2026
a7fe8ed
Add release workflow
bvscd Apr 2, 2026
1e888de
ci(node): fix runs-on to use runner group syntax
inyellowbus Apr 2, 2026
14d7a71
ci(node): fix runs-on to use runner group syntax
inyellowbus Apr 2, 2026
39d6b56
QUIC connection deduplication fix
bvscd Apr 2, 2026
75b311b
Merge pull request #54 from RSquad/quic_dedup
bvscd Apr 2, 2026
9c670a2
Rename colons in archives
mnogoborec Apr 3, 2026
57c2a08
docs(helm): mark ports.simplex as required for validators
inyellowbus Apr 3, 2026
549f15d
Review fix
mnogoborec Apr 3, 2026
4068797
fix masterchain ValueFlow burned fees and blackhole accounting
Lapo4kaKek Apr 3, 2026
aee43ad
fix: enforce mcStateExtra flags <=1 and remove ValidatorsStat
Lapo4kaKek Apr 4, 2026
41006d6
Merge pull request #55 from RSquad/rename-colons
bvscd Apr 4, 2026
3393e13
Merge branch 'release/node/v0.4.0' into fix-storage-phase
bvscd Apr 4, 2026
6802ece
Merge branch 'release/node/v0.4.0' into fix/valueflow-fee-burning-v0.4.0
yaroslavser Apr 4, 2026
1d124fb
Merge pull request #49 from RSquad/fix-storage-phase
bvscd Apr 4, 2026
e4fbe0b
fix(executor): preserve original due_payment for special accounts in …
Lapo4kaKek Apr 4, 2026
8fb327f
Implement burning coins
yaroslavser Apr 4, 2026
428afab
Implement burning coins
yaroslavser Apr 4, 2026
a76466a
accumulate fees_collected instead of overwriting to preserve shard burn
Lapo4kaKek Apr 4, 2026
0981ce4
Merge pull request #60 from RSquad/fix-storage-phase
bvscd Apr 4, 2026
73b783e
fix merge conflict in test_ordinary_transaction
Lapo4kaKek Apr 4, 2026
875d3cd
Merge release/node/v0.4.0 (fix-storage-phase updates)
Lapo4kaKek Apr 4, 2026
351295a
Merge pull request #59 from RSquad/fix/mc-state-extra-flags-compat
bvscd Apr 4, 2026
a2f0540
Merge pull request #58 from RSquad/fix/valueflow-fee-burning-v0.4.0
bvscd Apr 4, 2026
67c6364
Updates for Simplex. Support of separate address for QUIC
bvscd Apr 4, 2026
386413e
Merge pull request #61 from RSquad/simplex_upgrade
bvscd Apr 4, 2026
a1caff3
chore(node): changelog and archival node docs for v0.4.0
inyellowbus Apr 5, 2026
b08fead
Merge branch 'master' into release/node/v0.4.0
bvscd Apr 5, 2026
aba9620
Merge pull request #63 from RSquad/release/node/v0.4.0
bvscd Apr 5, 2026
9a9bcc6
chore(helm): bump appVersion and image tag to v0.4.0 (#64)
inyellowbus Apr 5, 2026
a80207c
feat(node): switch to ubuntu base image and add console binary
inyellowbus Apr 7, 2026
4ef0fcf
fix: max factor load from config
mrnkslv Apr 8, 2026
fa35c52
Merge branch 'master' into feature/sma-54-max-factor-upper-bound-is-h…
mrnkslv Apr 8, 2026
e4302c8
Revert "Merge branch 'master' into feature/sma-54-max-factor-upper-bo…
mrnkslv Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/node-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|----------|-------------|
Expand Down Expand Up @@ -1403,7 +1403,7 @@ Automatic elections task configuration:
- `"minimum"` — use minimum required stake
- `{ "fixed": <amount> }` — 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)
Expand Down Expand Up @@ -1720,7 +1720,7 @@ nodectl config wallet stake -b <BINDING> -a <AMOUNT> [-m <MAX_FACTOR>]
|------|------|----------|---------|-------------|
| `-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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -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
Expand Down
47 changes: 42 additions & 5 deletions src/node-control/common/src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ fn default_workchain() -> i32 {
-1
}

fn default_max_factor() -> f32 {
pub fn default_max_factor() -> f32 {
3.0
}

Expand Down Expand Up @@ -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<f32>) -> 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]");
}
Expand Down Expand Up @@ -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(())
}
}
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions src/node-control/common/src/ton_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
30 changes: 22 additions & 8 deletions src/node-control/elections/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) => {
Expand All @@ -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);
}
}
Expand All @@ -726,17 +728,24 @@ 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(())
}
}
}

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?;
Expand Down Expand Up @@ -782,6 +791,7 @@ impl ElectionRunner {
node_id: &str,
node: &mut Node,
stake: u64,
network_max_stake_factor: u32,
) -> anyhow::Result<Cell> {
let Some(participant) = &mut node.participant else {
anyhow::bail!("node [{}] no participant info", node_id);
Expand All @@ -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!("<max-factor> 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!(
"<max-factor> 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
Expand Down
6 changes: 3 additions & 3 deletions src/node-control/elections/src/runner_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down
18 changes: 18 additions & 0 deletions src/node-control/service/src/runtime_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand All @@ -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 =
Expand Down
14 changes: 14 additions & 0 deletions src/node-control/ton-http-api-client/src/v2/client_json_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32> {
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<f32> {
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,
Expand Down