-
Notifications
You must be signed in to change notification settings - Fork 4
Rust validator replay uses block-header gen_software instead of ConfigParam(8) for executor gates #28
Description
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:
- a candidate block reaches replay with
gen_softwaremetadata that does not exactly matchConfigParam(8) - 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