Feature/sma 4 add nominator pool support#39
Feature/sma 4 add nominator pool support#39mrnkslv wants to merge 15 commits intorelease/nodectl/v0.4.0from
Conversation
There was a problem hiding this comment.
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
NominatorPoolWrapperImplwrapper 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.
Made-with: Cursor
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| min_nominator_stake, | ||
| } => { | ||
| let configured_validator = | ||
| addresses[0].parse::<MsgAddressInt>().context("invalid TONCore addresses[0]")?; |
There was a problem hiding this comment.
use with_context(|| format!("invalid pool address: {}", addresses[0]))
| min_validator_stake, | ||
| min_nominator_stake, | ||
| } => { | ||
| let configured_validator = |
There was a problem hiding this comment.
Why it is called `XXX_validator? It's a pool address, isn't it?
| 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)")] |
There was a problem hiding this comment.
Use a clear description for that option.
| 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, |
There was a problem hiding this comment.
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).
src/node-control/README.md
Outdated
| - `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) |
There was a problem hiding this comment.
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 TONThe 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.
| /// Validator sends coins to increase their own stake in the pool. | ||
| /// Attach the desired amount of TON to the message; 1 TON is deducted as a processing fee. | ||
| pub fn deposit_validator(query_id: u64) -> anyhow::Result<Cell> { | ||
| let mut builder = BuilderData::new(); |
There was a problem hiding this comment.
Missing integration: deposit_validator / withdraw_validator are defined but never wired to any CLI command or automated flow.
After deployment, the pool starts with validator_amount = 0. The pool contract will reject every new_stake at:
throw_unless(83, validator_amount >= min_validator_stake); // 0 >= 100K TON → FAIL
Without a CLI command (e.g. nodectl wallet deposit-validator) or automated deposit step, the pool is deployed but cannot participate in elections. This is a blocker for the feature.
| max_nominators_count, | ||
| min_validator_stake, | ||
| max_nominators_stake: min_nominator_stake, | ||
| }, |
There was a problem hiding this comment.
Confusing field reuse. max_nominators_stake (from the SNP model) stores min_nominator_stake for TONCore — semantically opposite names.
Anyone consuming PoolData downstream will read max_nominators_stake and assume it means a maximum, not a minimum. Consider renaming the shared field or adding a method that returns the correct semantic value per pool kind.
| TONCore { | ||
| addresses: [String; 2], | ||
| validator_share: u64, | ||
| /// Deploy-time pool parameters; if omitted, defaults are applied in `contracts` (`resolve_deploy_pool_params`). |
There was a problem hiding this comment.
Loose typing. validator_share is u64 here but must fit u16 on-chain (pool.fc stores it as store_uint(validator_reward_share, 16), range 0..=10000 basis points).
Every use site does u16::try_from(*validator_share) — a value like 70000 is accepted at config parse time but fails at deploy/runtime. Using u16 directly (or a serde deserialize validator) would catch this earlier.
| )] | ||
| owner: Option<String>, | ||
| #[arg(long = "addr1", help = "Core: addresses[0] (with --kind core)")] | ||
| addr1: Option<String>, |
There was a problem hiding this comment.
Unclear naming. --addr1 / --addr2 with help text addresses[0] / addresses[1] does not tell the operator what these addresses represent.
Since both should be pool contract addresses (for even/odd validation rounds), consider naming them --pool-addr-even / --pool-addr-odd or at minimum documenting what they are in the help text.
| min_nominator_stake, | ||
| } => { | ||
| let configured_validator = MsgAddressInt::from_str(addresses[0].as_str()) | ||
| .context(format!("invalid TONCore addresses[0]: {}", addresses[0]))?; |
There was a problem hiding this comment.
Copy-paste: this ~25-line block is duplicated in 3 files.
The same sequence — parse addresses[0], validate against validator, convert validator_share to u16, call resolve_deploy_pool_params, call calculate_address, validate addresses[1] — is repeated in:
deploy_cmd.rs:330-367runtime_config.rs:522-556(here)config_wallet_cmd.rs:710-746
Only the post-validation action differs (deploy / open wrapper / return address). Consider extracting a helper into the contracts crate, e.g.:
pub fn resolve_toncore_pool(
config: &PoolConfig::TONCore,
validator_addr: &MsgAddressInt,
) -> anyhow::Result<(MsgAddressInt, u16, u16, u64, u64)>that validates addresses, converts types, resolves defaults, and returns the resolved params.
| ) | ||
| .map_err(set_err)?; | ||
| let state_init = NominatorPoolWrapperImpl::build_state_init( | ||
| &validator_addr, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/node-control/README.md
Outdated
| - `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) | ||
|
|
There was a problem hiding this comment.
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.
Summary
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 + defaultscontracts/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 Routercontracts/src/nominator/single_nominator.rs— extracted new_with_state_init constructorcontracts/src/lib.rs, ton_core_nominator.rs— module wiring and re-exportscommon/src/app_config.rs— TONCore and TONCoreRouter variants with optional deploy fields (max_nominators, min_* stakes, addresses: Option<[Option; 2]>), serde tests for all variantscommands/.../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-indexcommands/.../config_wallet_cmd.rs— wallet stake with --pool-index for Router target selection; resolve_pool_address handles all pool typescommands/.../deploy_cmd.rs— deploy TONCore / TONCoreRouter (sequential multi-target deploy with seqno increment); SNP unchangedcommands/.../config_cmd.rs— pass CancellationCtx to pool subcommands for deposit-validator / withdraw-validatorelections/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 matchingelections/src/runner_tests.rs— 4 Router-specific tests: free pool selection, both-busy error, dual recovery, finished-elections matching on either pool addresselections/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 HashMapservice/src/auth/user_store.rs— updated mock pools() return typeREADME.md— pools schema for core and core_routerCloses SMA-4