Skip to content

Feature/sma 4 add nominator pool support#39

Open
mrnkslv wants to merge 8 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 8 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.

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 (+ unit tests on cells)
  • contracts/src/lib.rs, ton_core_nominator.rs — module wiring and re-exports
  • common/src/app_config.rs — TONCore optional deploy fields (max_nominators, min_* stakes), serde tests including omitted vs explicit JSON keys
  • commands/.../config_pool_cmd.rs — config pool add --kind core, resolved deploy params in success message, pool ls for Core
  • commands/.../deploy_cmd.rs — deploy TON Nominator Pool without --owner; SNP unchanged
  • commands/.../config_wallet_cmd.rs — manual stake path validates addresses[0]/[1] vs calculate_address for core
  • service/src/runtime_config.rs — open_nominator_pool for TONCore via NominatorPoolWrapperImpl
  • README.md — pools schema for core (addresses, validator_share, optional deploy fields)

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

/// 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();
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.

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,
},
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.

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.

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

TONCore {
addresses: [String; 2],
validator_share: u64,
/// Deploy-time pool parameters; if omitted, defaults are applied in `contracts` (`resolve_deploy_pool_params`).
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.

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.

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

)]
owner: Option<String>,
#[arg(long = "addr1", help = "Core: addresses[0] (with --kind core)")]
addr1: 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.

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.

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_nominator_stake,
} => {
let configured_validator = MsgAddressInt::from_str(addresses[0].as_str())
.context(format!("invalid TONCore addresses[0]: {}", 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.

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-367
  • runtime_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.

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

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