Feature/sma 4 add nominator pool support#39
Feature/sma 4 add nominator pool support#39mrnkslv wants to merge 19 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.
src/node-control/commands/src/commands/nodectl/config_pool_cmd.rs
Outdated
Show resolved
Hide resolved
| ) | ||
| .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.
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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>, |
| #[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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
The same suggestion as above.
| /// Nominator Pool (`kind: "core"` / `PoolConfig::TONCore`) | ||
| Core, | ||
| /// Two-pool router (`kind: "core_router"` / `PoolConfig::TONCoreRouter`) | ||
| Router, |
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
--address-even and --address-odd
| min_validator_stake: Option<u64>, | ||
| #[serde(default)] | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| min_nominator_stake: Option<u64>, |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
The name is confusing. Rename to calculate_address_and_state
| @@ -0,0 +1,17 @@ | |||
| /* | |||
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
We should't use function of concrete pool implementation here. Instead, move common function to the parent level ( wrapper.rs).
| @@ -0,0 +1,212 @@ | |||
| /* | |||
There was a problem hiding this comment.
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>> { |
There was a problem hiding this comment.
I think we should't modify this trait. Instead, create a TonCoreNominatorRouter struct and implement a NominatorWrapper trait for it.
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