name: tn-domain-execution description: | Domain expert reference for the telcoin-network execution layer — Reth integration, EVM block production, payload building, and the executor/engine boundary contract. Loaded by tn-rust-engineer and tn-domain-reviewer when work touches reth_env, payload builder, batch builder's block-shaping logic, base fee derivation, or any code that reads or writes EVM state. NOT user-invocable. Loaded programmatically by tn-* agents via the Skill tool.
tn-domain-execution
The execution layer in telcoin-network is a Reth-backed EVM that consumes ConsensusOutput and produces blocks. It runs behind consensus — the consensus DB always knows about more headers than the engine has executed. That asynchrony is the single most important fact about this layer; almost every divergence bug in execution is a misuse of state at the consensus/execution boundary.
If you are about to modify code that:
- lives under
crates/engine/**,crates/tn-reth/**,crates/batch-builder/** - calls
reth_env.*for headers, state, finality, or worker fee configs - builds, validates, or sequences EVM blocks (
payload_builder.rs, block executor) - derives base fees, applies system calls, or computes state roots
- handles canonical reorgs, state restoration, or
try_restore_state
…load this skill before writing a single line.
Why execution is different
The engine is a follower of consensus. Consensus commits a ConsensusOutput, persists it to the consensus DB, and forwards it to the engine via mpsc. The engine then builds and executes the block. Crash recovery, mode changes, and slow execution can all leave the engine arbitrarily behind consensus.
That means three things must always be true of every read in this layer:
- Reads of "the canonical tip" reflect engine progress, not consensus progress. The chain you see may be many consensus outputs behind.
- Reads of "the finalized header" can return
Noneon fresh restart, or return a header much older than the latest committed consensus. - Reads of contract storage at canonical tip reflect any system-call mutations the engine has executed — but not ones from outputs the engine hasn't processed yet.
Code that mixes "look at canonical tip" with "look at consensus DB" without understanding which one is ahead is the breeding ground for forks.
Invariants
reth_env.finalized_header()is best-effort, not authoritative. It may returnNone, return a stale header, or skip ahead at a reorg. Never use it as the sole source for cross-validator state. If you need a specific block's state, fetch by number or hash and explicitly handle the missing case.The closing-epoch final block is the canonical snapshot for boundary-derived values. Base fee carryover, worker fee configs, and any system-call output for epoch N must be read from the state at the last block of epoch N — not canonical tip, not the opening block of N+1. (Cross-references
tn-domain-epochinvariant I-1; both skills enforce this from their respective sides.)Block production is deterministic across all validators given identical input. That requires: identical transaction order, identical timestamp, identical base fee, identical beneficiary, identical system-call results. Any non-deterministic source (HashMap iteration over a transaction set,
SystemTime::now()for the block timestamp, a random gas-limit perturbation) is a fork.payload_buildermust apply system calls in a fixed order at fixed boundaries.applyIncentivesruns beforeconcludeEpochin the closing block;applySlashesruns at the position consensus dictates. The order is part of the protocol — reordering is a state-root divergence even if the same calls all fire.Engine state is read-only outside the executor task. Any other task that reads engine state via
recent_blockswatch channels, the consensus bus, orlast_executed_consensus_numbermust accept that the value can advance between consecutive reads. Don't compute over engine state acrossawaitpoints without re-reading.
Pre-write Checklist
Where is the engine right now? Specifically: which is the highest consensus number it has executed? Are you reading state from after that point? If yes, what guarantees that state exists?
What's the determinism story? List every input that feeds the value you're computing. For each, name the source and confirm all validators see the same value. If a
HashMap,SystemTime, RNG, or thread-ordering dependency appears, stop and switch sources.Does this run in the executor task or outside? If outside, you cannot mutate engine state. You can only consume it via the channels and watches the engine exposes.
Does this read contract storage? If yes: at what block? "Canonical tip" is rarely the right answer. The closing-epoch final block, an explicit historical block, or a system-call result snapshot are the canonical alternatives.
Does this fall back when state is missing? If yes: would all validators compute the same fallback under identical conditions? If not, this is a forking default. Replace with an explicit error or defer the operation.
Does this respect the
applyIncentives→concludeEpochordering at the closing block? If you're touching boundary block construction, list every system call you fire and verify the order matches the protocol.
Canonical Sources
| Value | Read from | Do NOT read from |
|---|---|---|
| EVM state at block N | reth_env.state_at_block(N) (after verifying N exists) |
Implicit "canonical tip" reads when N is what you actually want |
| Worker fee configs at boundary | reth_env.get_worker_fee_configs_at(closing_final_height, ...) |
reth_env.get_worker_fee_configs(...) (reads tip) |
| Base fee for opening epoch N+1 | header.base_fee_per_gas of last block of epoch N |
A computed/derived fallback if the header is missing — defer instead |
| Last executed consensus number | engine.last_executed_consensus_number() |
Any cached counter elsewhere; the engine is the source of truth |
| Block timestamp | consensus_output.committed_at (consensus-supplied) |
SystemTime::now() in any code path |
| Transaction order in a block | The order specified by ConsensusOutput.batches |
Any iteration order over a HashMap, HashSet, or unordered collection |
| Beneficiary (block producer) | Resolved from the leader of the consensus round, validated against committee at boundary | Any address from local config or RPC input |
Common Bug Patterns
Pattern 1: Using finalized_header() where a specific block is needed
// WRONG — finalized_header returns "some" finalized block, not the boundary block
if let Some(block) = reth_env.finalized_header()? {
let configs = reth_env.get_worker_fee_configs(num_workers)?;
// configs are read from canonical tip, not from `block`
}
The fix: explicitly compute the height you want, fetch by number, and read everything from that pinned point. finalized_header() is appropriate only when "any recent finalized block" is genuinely what you need (e.g., for a non-deterministic display value), which is rare in protocol code.
Pattern 2: Non-deterministic block input
// WRONG — HashMap iteration order is non-deterministic across runs
let txs: HashMap<TxHash, Tx> = ...;
for (_, tx) in txs.iter() {
block.add_tx(tx);
}
The fix: iterate in a defined order (sort by hash, by sender+nonce, or by the order consensus supplied). Any time you see HashMap/HashSet near block construction, treat it as a divergence flag.
Pattern 3: Reordering system calls at the closing block
// WRONG — concludeEpoch must run AFTER applyIncentives, not before
fn build_closing_block(...) {
apply_system_call(SystemCall::ConcludeEpoch(new_committee));
apply_system_call(SystemCall::ApplyIncentives(rewards));
}
The fix: incentives credit accounts using the closing committee's stake; concludeEpoch then transitions to the new committee. Reversing the order pays incentives to the new committee using the new stake versions, which is wrong and produces a different state root from the rest of the network.
Pattern 4: Crossing await points without re-reading engine state
// WRONG — engine state may have advanced during `slow_op().await`
let last = engine.last_executed_consensus_number();
let response = slow_op().await?;
do_work_assuming(last); // `last` may now be stale
The fix: re-read engine state after every await if its freshness matters, or take a snapshot upfront and document that the snapshot is what the operation operates on (not "current" state).
Further Reading
references/invariants.md— every invariant with code-pointer to where it must holdreferences/bug-patterns.md— historical bugs caught by this skillreferences/canonical-sources.md— full value-to-source lookup with file paths