Skip to content

Rust validator replay uses block-header gen_software instead of ConfigParam(8) for executor gates #28

@EmelyanenkoK

Description

@EmelyanenkoK

Summary

Rust does not use the same source of truth as C++ for transaction replay version/capability gates.

In Rust, validator replay constructs executor configuration from the candidate block header's optional gen_software field. In C++, replay uses the authoritative masterchain configuration (ConfigParam(8)).

That means the same candidate block can be replayed under different rules by Rust and C++, even when both validators start from the same masterchain state.

Affected code

  • Rust: rust_implementation/ton-rust-node/src/node/src/validator/validate_query.rs:765-799
  • Rust: rust_implementation/ton-rust-node/src/node/src/validator/validate_query.rs:5540-5551
  • Rust: rust_implementation/ton-rust-node/src/executor/src/blockchain_config.rs:197-220
  • Rust: rust_implementation/ton-rust-node/src/executor/src/blockchain_config.rs:280-293
  • Rust: rust_implementation/ton-rust-node/src/executor/src/transaction_executor.rs:418-430
  • Rust: rust_implementation/ton-rust-node/src/executor/src/transaction_executor.rs:829-836
  • Rust: rust_implementation/ton-rust-node/src/executor/src/transaction_executor.rs:1211-1215
  • C++: validator/impl/validate-query.cpp:1016-1043
  • C++: validator/impl/validate-query.cpp:1075-1149

Rust/C++ discrepancy

Rust replay does:

let (capabilities, block_version) =
    base.info.gen_software().map_or((0, 0), |v| (v.capabilities, v.version));
let config =
    BlockchainConfig::with_params(capabilities, block_version, base.config_params.clone())?;

BlockchainConfig then uses those header-derived values as the effective replay gates.

C++ does not use header metadata for this. It threads the masterchain configuration into replay:

  • config_->get_global_version()
  • config_->has_capability(...)

and uses that same config-derived source for storage, compute, action, and serialization settings.

So the two implementations disagree on what controls replay semantics:

  • Rust: untrusted block header metadata
  • C++: authoritative masterchain config

Why this matters

Those version/capability gates are not cosmetic. They directly control executor behavior, including examples such as:

  • CapBounceMsgBody
  • version-gated message rewrite behavior at block_version() >= 8
  • version-gated behavior at block_version() >= 11
  • VM/executor capability toggles used during replay

If gen_software is stale, inconsistent with ConfigParam(8), or otherwise not the same source that C++ uses, Rust and C++ can execute the same transaction under different rules.

Trigger

The discrepancy is reachable when:

  1. a candidate block reaches replay with gen_software metadata that does not exactly match ConfigParam(8)
  2. at least one transaction in the block depends on a version/capability-gated executor path

An attacker can try to trigger this by crafting header metadata. More importantly, the implementation is unsound even without a specific exploit because replay is reading semantics from the wrong object.

Why this can stop a mixed Rust/C++ network

Once Rust and C++ replay under different version/capability gates, they can derive different:

  • transaction descriptors
  • outbound messages
  • fees and balances
  • account states
  • final state roots

At that point, the candidate block becomes implementation-dependent:

  • a Rust collator/validator can accept or build a block under Rust header-driven semantics that C++ rejects under config-driven semantics
  • a C++ collator/validator can accept or build a block under config-driven semantics that Rust rejects under header-driven semantics

That is enough to split validator votes on the same block. In a mixed validator set, one such candidate can stall consensus; with competing majorities, it can fork the network.

Suggested fix

Rust validator replay should take replay gates only from the authoritative masterchain configuration:

  • use base.config_params.global_version()
  • use base.config_params.capabilities()
  • stop deriving replay semantics from base.info.gen_software()

The block header may be logged or sanity-checked, but it must not control deterministic executor rules.

Suggested regression test

Build a replay fixture where ConfigParam(8) enables a capability or version-gated rule, but the block header carries different gen_software values.

The test should verify that:

  • current Rust replay follows header metadata
  • C++ replay follows ConfigParam(8)
  • after the fix, Rust matches C++ and ignores header metadata for executor semantics

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions