Summary
Rust short-circuits the storage phase for special accounts and collects no storage fees at all. C++ does not. C++ still computes and collects storage fees for special accounts, and it still clears previously accumulated due_payment when the balance covers it. Only freeze/delete transitions are treated specially there.
Because storage fees and due_payment feed directly into replayed transaction output and account state, this discrepancy is consensus-critical.
Affected code
- Rust:
rust_implementation/ton-rust-node/src/executor/src/transaction_executor.rs:163-189
- Rust:
rust_implementation/ton-rust-node/src/vm/src/smart_contract_info.rs:104-112
- C++:
crypto/block/transaction.cpp:1056-1123
Rust/C++ discrepancy
Rust exits early for is_special:
if is_special {
return Ok(TrStoragePhase::with_params(
Coins::zero(),
acc.due_payment().cloned(),
AccStatusChange::Unchanged,
));
}
That means Rust does not:
- compute current storage fees
- add prior
due_payment
- debit balance for storage fees
- clear
due_payment when it is covered
C++ still performs fee accounting for special accounts:
- computes current storage fees
- adds previous
due_payment
- collects what can be collected
- clears
due_payment when fully covered
The special-account exemption in C++ only suppresses freeze/delete consequences and last_paid handling. It is not a full storage-phase bypass.
Trigger
The discrepancy is reachable on any special-account transaction where storage accounting is non-zero, including:
- tick-tock execution
- ordinary execution paths that run on special accounts
- cases with existing non-zero
due_payment
- cases where current storage fees accrued since
last_paid
This does not require an invalid block or malformed transaction. Honest chain activity is enough once a special account reaches one of those states.
Why this can stop a mixed Rust/C++ network
For the same special-account transaction, Rust and C++ can derive different values for:
storage_fees_collected
storage_fees_due
- account balance after storage phase
- persisted
due_payment
- TVM-visible
DUEPAYMENT in the contract context
- final account state and state root
Because Rust also exposes account.due_payment() through SmartContractInfo, the discrepancy is not limited to the storage-phase descriptor. It can change later compute/action behavior inside the same transaction.
That makes the replay result implementation-dependent:
- a Rust validator replaying a C++-produced block can compute a different transaction result and reject the block
- a C++ validator replaying a Rust-produced block can do the same in the opposite direction
In a mixed validator set, a single block containing such a special-account transaction can split validator votes and stop consensus. No attacker is required if the relevant special-account state occurs naturally; an attacker only makes the trigger easier.
Suggested fix
Remove the Rust early return for is_special.
Rust should match C++ by:
- computing storage fees for special accounts
- adding existing
due_payment
- debiting balance
- clearing
due_payment when covered
- keeping only the C++ special-case behavior for freeze/delete handling and related status transitions
Suggested regression test
Build a replay fixture for a special account with non-zero storage debt or accrued storage fees.
The test should compare Rust and C++ on:
TrStoragePhase
- balance after transaction
- persisted
due_payment
- final transaction/state hash
Rust should match the C++ result exactly after the fix.
Summary
Rust short-circuits the storage phase for special accounts and collects no storage fees at all. C++ does not. C++ still computes and collects storage fees for special accounts, and it still clears previously accumulated
due_paymentwhen the balance covers it. Only freeze/delete transitions are treated specially there.Because storage fees and
due_paymentfeed directly into replayed transaction output and account state, this discrepancy is consensus-critical.Affected code
rust_implementation/ton-rust-node/src/executor/src/transaction_executor.rs:163-189rust_implementation/ton-rust-node/src/vm/src/smart_contract_info.rs:104-112crypto/block/transaction.cpp:1056-1123Rust/C++ discrepancy
Rust exits early for
is_special:That means Rust does not:
due_paymentdue_paymentwhen it is coveredC++ still performs fee accounting for special accounts:
due_paymentdue_paymentwhen fully coveredThe special-account exemption in C++ only suppresses freeze/delete consequences and
last_paidhandling. It is not a full storage-phase bypass.Trigger
The discrepancy is reachable on any special-account transaction where storage accounting is non-zero, including:
due_paymentlast_paidThis does not require an invalid block or malformed transaction. Honest chain activity is enough once a special account reaches one of those states.
Why this can stop a mixed Rust/C++ network
For the same special-account transaction, Rust and C++ can derive different values for:
storage_fees_collectedstorage_fees_duedue_paymentDUEPAYMENTin the contract contextBecause Rust also exposes
account.due_payment()throughSmartContractInfo, the discrepancy is not limited to the storage-phase descriptor. It can change later compute/action behavior inside the same transaction.That makes the replay result implementation-dependent:
In a mixed validator set, a single block containing such a special-account transaction can split validator votes and stop consensus. No attacker is required if the relevant special-account state occurs naturally; an attacker only makes the trigger easier.
Suggested fix
Remove the Rust early return for
is_special.Rust should match C++ by:
due_paymentdue_paymentwhen coveredSuggested regression test
Build a replay fixture for a special account with non-zero storage debt or accrued storage fees.
The test should compare Rust and C++ on:
TrStoragePhasedue_paymentRust should match the C++ result exactly after the fix.