Skip to content

Feature/sma 4 add nominator pool support#39

Open
mrnkslv wants to merge 19 commits intorelease/nodectl/v0.4.0from
feature/sma-4-add-nominator-pool-support
Open

Feature/sma 4 add nominator pool support#39
mrnkslv wants to merge 19 commits intorelease/nodectl/v0.4.0from
feature/sma-4-add-nominator-pool-support

Conversation

@mrnkslv
Copy link
Copy Markdown

@mrnkslv mrnkslv commented Mar 31, 2026

Summary

  • Add support for the TON Nominator Pool (kind: "core" / PoolConfig::TONCore) alongside existing Single Nominator Pool: deploy-time parameters resolved in contracts, shared elector opcodes with SNP, and CLI/service integration for pool config, deploy, wallet stake resolution, and runtime pool loading.
  • Add TONCoreRouter (kind: "core_router" / PoolConfig::TONCoreRouter) — a dual-pool extension of TONCore that manages two pools (pool[1] differs by min_validator_stake + 1), automatically routes stakes to the free pool via get_pool_data().state == 0, recovers from both, and enables uninterrupted validation across election rounds.

Changes

  • contracts/src/ton_core_nominator/ — NominatorPoolWrapperImpl (embedded pool code, build_state_init / calculate_address / from_init_data), get_pool_data / get_roles, resolve_deploy_pool_params + defaults
  • contracts/src/ton_core_nominator/messages.rs— pool-specific opcodes and message builders: new_stake, recover_stake, deposit_validator, withdraw_validator (+ unit tests on cells)
  • contracts/src/ton_core_nominator/ton_core_nominator.rs — resolve_toncore_pool (single pool), resolve_toncore_router (two pools with min_validator_stake + 1 for pool[1])
  • contracts/src/nominator/wrapper.rs — NodePools enum (Single / Router) with primary(), all(), select_free() methods; select_free() queries on-chain pool state for Router
  • contracts/src/nominator/single_nominator.rs — extracted new_with_state_init constructor
  • contracts/src/lib.rs, ton_core_nominator.rs — module wiring and re-exports
  • common/src/app_config.rs — TONCore and TONCoreRouter variants with optional deploy fields (max_nominators, min_* stakes, addresses: Option<[Option; 2]>), serde tests for all variants
  • commands/.../config_pool_cmd.rs — config pool add --kind core|router, --address-0/--address-1 for Router (rejects --address), pool ls table for Core/Router, deposit-validator/withdraw-validator with --pool-index
  • commands/.../config_wallet_cmd.rs — wallet stake with --pool-index for Router target selection; resolve_pool_address handles all pool types
  • commands/.../deploy_cmd.rs — deploy TONCore / TONCoreRouter (sequential multi-target deploy with seqno increment); SNP unchanged
  • commands/.../config_cmd.rs — pass CancellationCtx to pool subcommands for deposit-validator / withdraw-validator
  • elections/src/runner.rs — resolve_staking_target() picks the free pool once per tick, consistent across calc_stake, send_stake, and elector signature; recover_stake iterates all pools with per-tx gas tracking; snapshots use all_staking_addresses() for Router matching
  • elections/src/runner_tests.rs — 4 Router-specific tests: free pool selection, both-busy error, dual recovery, finished-elections matching on either pool address
  • elections/src/election_task.rs — pool type updated to HashMap<String, NodePools>
  • service/src/runtime_config.rs — open_nominator_pool returns NodePools::Single or NodePools::Router; pools stored as HashMap<String, NodePools>
  • service/src/contracts/contracts_task.rs — ensure_pools_deployed iterates node_pools.all() for multi-pool deployment
    -service/src/task/task_manager.rs — NoopRuntimeConfig::pools() returns empty HashMap
  • service/src/auth/user_store.rs — updated mock pools() return type
  • README.md — pools schema for core and core_router

Closes SMA-4

Copilot AI review requested due to automatic review settings March 31, 2026 07:51
@linear
Copy link
Copy Markdown

linear bot commented Mar 31, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for the TON Nominator Pool (also called TONCore pool or pool-core) to the node control system. Previously, only Single Nominator Pools (SNP) were supported. The implementation includes contract wrappers for pool interaction, message builders for pool operations, configuration support with optional deployment parameters, and CLI commands for pool management.

Changes:

  • Added NominatorPoolWrapperImpl wrapper for interacting with deployed TON Nominator Pools
  • Implemented pool contract state initialization with support for configurable deployment parameters (max nominators, min stakes)
  • Added message builders for pool operations (accept coins, process withdrawals, update validator set, etc.)
  • Extended configuration to support TONCore pools with optional deploy-time parameters
  • Updated deployment commands to support deploying TON Nominator Pools alongside SNP
  • Added configuration commands for managing core pools in the CLI
  • Updated documentation with TONCore pool configuration details

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/node-control/contracts/src/ton_core_nominator/ New module implementing nominator pool wrapper and message builders with comprehensive tests
src/node-control/service/src/runtime_config.rs Added support for opening TONCore pools during runtime configuration
src/node-control/common/src/app_config.rs Extended PoolConfig enum with TONCore variant including optional deployment parameters, with serialization tests
src/node-control/commands/src/commands/nodectl/deploy_cmd.rs Updated pool deployment command to handle both SNP and TONCore pools
src/node-control/commands/src/commands/nodectl/config_wallet_cmd.rs Added TONCore pool support for manual stake operations
src/node-control/commands/src/commands/nodectl/config_pool_cmd.rs Added CLI commands for managing core pools with dedicated configuration options
src/node-control/contracts/src/lib.rs Exported new nominator pool types and functions
src/node-control/README.md Updated documentation for TONCore pool configuration

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

mrnkslv and others added 3 commits March 31, 2026 11:02
Made-with: Cursor
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@mrnkslv mrnkslv requested review from a team, ITBear, Keshoid and Lapo4kaKek and removed request for a team March 31, 2026 10:28
min_nominator_stake,
} => {
let configured_validator =
addresses[0].parse::<MsgAddressInt>().context("invalid TONCore addresses[0]")?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use with_context(|| format!("invalid pool address: {}", addresses[0]))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

min_validator_stake,
min_nominator_stake,
} => {
let configured_validator =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it is called `XXX_validator? It's a pool address, isn't it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's true

owner: Option<String>,
#[arg(long = "addr1", help = "Core: addresses[0] (with --kind core)")]
addr1: Option<String>,
#[arg(long = "addr2", help = "Core: addresses[1] (with --kind core)")]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a clear description for that option.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if validator_addr != wallet_address {
return Err(set_err(anyhow::anyhow!(
"TONCore addresses[0] must match this node's wallet (expected {}, config has {})",
wallet_address,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design issue: addresses[0] is not the validator wallet.

Both addresses[0] and addresses[1] should be pool contract addresses (for even/odd validation rounds, as pool.fc itself notes: "The validator in most cases have two pools").

The validator wallet address is already available from the wallets config section via the node binding — no need to store it in the pool config.

This same wrong assumption (addresses[0] == validator wallet) is repeated in runtime_config.rs:525-531 and config_wallet_cmd.rs:716-722.

Fix: get the validator address from the wallet config (already resolved as wallet_address here), and treat both addresses[0] and addresses[1] as the two pool contract addresses. Derive/validate both against (wallet_address, validator_share, deploy_params).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

- `validator_share` — validator reward share (basis points; stored as `u16` on-chain)
- `max_nominators` — optional; if omitted, defaults from the contracts module are used (40 nominators)
- `min_validator_stake` — optional (nanotons); if omitted, defaults from the contracts module are used (100 TON)
- `min_nominator_stake` — optional (nanotons); if omitted, defaults from the contracts module are used (10 TON)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: defaults are off by 1000x.

DEFAULT_DEPLOY_MIN_VALIDATOR_STAKE_NANOTONS: u64 = 100_000_000_000_000; // = 100,000 TON
DEFAULT_DEPLOY_MIN_NOMINATOR_STAKE_NANOTONS: u64 = 10_000_000_000_000;  // = 10,000 TON

The README says 100 TON / 10 TON but the actual code defaults are 100,000 TON / 10,000 TON. This will mislead operators who skip the optional fields.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

)
.map_err(set_err)?;
let state_init = NominatorPoolWrapperImpl::build_state_init(
&validator_addr,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant work: build_state_init is called twice with the same arguments.

calculate_address (line 354) already calls build_state_init internally to hash the StateInit into an address. Then build_state_init is called again here with identical parameters.

Use from_init_data instead (which stores both the address and the StateInit), or add a method that returns (MsgAddressInt, StateInit) in one pass.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1341 to 1357
- `max_nominators` — optional; if omitted, defaults from the contracts module are used (40 nominators)
- `min_validator_stake` — optional (nanotons); if omitted, defaults from the contracts module are used (100,000 TON)
- `min_nominator_stake` — optional (nanotons); if omitted, defaults from the contracts module are used (10,000 TON)

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README documentation is incomplete and inaccurate for the pools section. Line 1341 states "Two pool types are supported" but the implementation now supports three: SNP, TONCore, and TONCoreRouter. Additionally, the TONCore documentation at line 1352 incorrectly states "addresses — two addresses: validator wallet ([0]) and pool contract ([1]...)" but the actual PoolConfig::TONCore only has a single address: Option<String> field. The variant with two addresses is PoolConfig::TONCoreRouter. The section is also missing complete documentation for the TONCoreRouter variant, which should document the addresses: Option<[Option<String>; 2]> field and explain how the router automatically selects free pools.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

#[arg(long = "address-0", help = "Router: pool[0] address (optional; derived if omitted)")]
address_0: Option<String>,
#[arg(long = "address-1", help = "Router: pool[1] address (optional; derived if omitted)")]
address_1: Option<String>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to name this options as --address-even / --address-odd and update help messages, e.g. "Pool address for an even validation round"

long = "min-validator-stake-nano",
help = "Core/Router: min validator stake in nanotons (embedded at deploy)"
)]
min_validator_stake_nano: Option<u64>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use min_validator_stake and then convert from tons to nano in code. Because we already have commands which accepts tons.

long = "min-nominator-stake-nano",
help = "Core/Router: min nominator stake in nanotons (embedded at deploy)"
)]
min_nominator_stake_nano: Option<u64>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same issue as above.

#[arg(short = 'a', long = "amount", help = "Amount in TON to deposit")]
amount: f64,
#[arg(long = "pool-index", default_value_t = 0, help = "Router: pool index (0 or 1)")]
pool_index: usize,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename to --pool-even and --pool-odd

#[arg(short = 'a', long = "amount", help = "Amount in TON to withdraw")]
amount: f64,
#[arg(long = "pool-index", default_value_t = 0, help = "Router: pool index (0 or 1)")]
pool_index: usize,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same suggestion as above.

/// Nominator Pool (`kind: "core"` / `PoolConfig::TONCore`)
Core,
/// Two-pool router (`kind: "core_router"` / `PoolConfig::TONCoreRouter`)
Router,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you to remove this kind and use Core only. But inside the code we must always use Router no matter how many addresses the user specifies.

info,
)
}
PoolAddKind::Core => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This match branch slightly duplicates a PoolAddKind::Router branch - so I suggest to use only PoolAddKind::Core and stores addresses as vector with 1 or 2 elements.

}
PoolAddKind::Router => {
if self.address.is_some() {
anyhow::bail!("For router: use --address-0 / --address-1 instead of --address");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--address-even and --address-odd

min_validator_stake: Option<u64>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
min_nominator_stake: Option<u64>,
Copy link
Copy Markdown
Contributor

@Keshoid Keshoid Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor suggestion. Let's remove Option from every field and define real default values instead of #[default].

/// Two TONCore pools with automatic routing: pool[0] uses `min_validator_stake`,
/// pool[1] uses `min_validator_stake + 1`. The runner picks the first pool with `state == 0`.
#[serde(rename = "core_router")]
TONCoreRouter {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you to remove this variant and use TONCore only.

}

/// Calculate both the pool address and `StateInit` in a single pass.
pub fn calculate_address_with_state_init(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name is confusing. Rename to calculate_address_and_state

@@ -0,0 +1,17 @@
/*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module has the same name as the file inside the module. Use different names.

let (max_n, min_v, min_n) =
resolve_deploy_pool_params(max_nominators, min_validator_stake, min_nominator_stake);

let (address, state_init) = NominatorPoolWrapperImpl::calculate_address_with_state_init(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should't use function of concrete pool implementation here. Instead, move common function to the parent level ( wrapper.rs).

@@ -0,0 +1,212 @@
/*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this message function to already existing messages.rs and add comments that they are for ton core pool only.

}
fn pools(&self) -> Arc<HashMap<String, Arc<dyn NominatorWrapper>>> {
unimplemented!()
fn pools(&self) -> Arc<HashMap<String, NodePools>> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should't modify this trait. Instead, create a TonCoreNominatorRouter struct and implement a NominatorWrapper trait for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants