Skip to content
Open
23 changes: 18 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -161,19 +161,32 @@ TN_SEED_SIGNATURE_FORK_EPOCH ?= 4294967295
# multi_workers_override_is_inert_when_unset requires a process WITHOUT the variable.
TN_MULTI_WORKERS_FORK_EPOCH ?= 4294967295

# PREVRANDAO fork epoch for the e2e lanes (#1247). Same shape as the two siblings above,
# defaulting to u32::MAX so the default lanes run the fork DORMANT (legacy
# `output_digest ^ batch_digest` mix hash). Unlike the siblings this fork does NOT arm
# independently: `prevrandao_seed_active` is the fail-closed conjunction of this fork point
# and the seed-signature fork, and non-adiri builds leave the fork point active from
# genesis, so without this pin a lane arming the seed fork alone would silently arm the
# seeded PREVRANDAO derivation too. A lane that wants it arms both knobs deliberately:
# TN_SEED_SIGNATURE_FORK_EPOCH=0 TN_PREVRANDAO_FORK_EPOCH=0 make test-e2e
# Only test-utils builds consult it (tn_types::forks::prevrandao_fork_epoch_override).
# Set only on name-filtered nextest lines, never a bare --workspace run: tn-types'
# prevrandao_override_is_inert_when_unset requires a process WITHOUT the variable.
TN_PREVRANDAO_FORK_EPOCH ?= 4294967295

# run restart integration tests
test-restarts: build-e2e-bin
TN_BIN_PATH="$(E2E_BIN)" TN_SEED_SIGNATURE_FORK_EPOCH=$(TN_SEED_SIGNATURE_FORK_EPOCH) TN_MULTI_WORKERS_FORK_EPOCH=$(TN_MULTI_WORKERS_FORK_EPOCH) cargo nextest run --run-ignored all test_restarts ;
TN_BIN_PATH="$(E2E_BIN)" TN_SEED_SIGNATURE_FORK_EPOCH=$(TN_SEED_SIGNATURE_FORK_EPOCH) TN_MULTI_WORKERS_FORK_EPOCH=$(TN_MULTI_WORKERS_FORK_EPOCH) TN_PREVRANDAO_FORK_EPOCH=$(TN_PREVRANDAO_FORK_EPOCH) cargo nextest run --run-ignored all test_restarts ;

# run epoch integration tests (same filter as the public-tests epoch line). The scheduled
# Durable e2e lane (#1149) runs this and test-restarts with TN_TEST_MDBX_SYNC=durable
# exported so every spawned node opens MDBX in Durable.
test-epochs: build-e2e-bin
TN_BIN_PATH="$(E2E_BIN)" TN_SEED_SIGNATURE_FORK_EPOCH=$(TN_SEED_SIGNATURE_FORK_EPOCH) TN_MULTI_WORKERS_FORK_EPOCH=$(TN_MULTI_WORKERS_FORK_EPOCH) cargo nextest run -p e2e-tests --test it --run-ignored all test_epoch ;
TN_BIN_PATH="$(E2E_BIN)" TN_SEED_SIGNATURE_FORK_EPOCH=$(TN_SEED_SIGNATURE_FORK_EPOCH) TN_MULTI_WORKERS_FORK_EPOCH=$(TN_MULTI_WORKERS_FORK_EPOCH) TN_PREVRANDAO_FORK_EPOCH=$(TN_PREVRANDAO_FORK_EPOCH) cargo nextest run -p e2e-tests --test it --run-ignored all test_epoch ;

# run e2e tests
test-e2e: build-e2e-bin
TN_BIN_PATH="$(E2E_BIN)" TN_SEED_SIGNATURE_FORK_EPOCH=$(TN_SEED_SIGNATURE_FORK_EPOCH) TN_MULTI_WORKERS_FORK_EPOCH=$(TN_MULTI_WORKERS_FORK_EPOCH) cargo nextest run -p e2e-tests --run-ignored ignored-only --all-features ;
TN_BIN_PATH="$(E2E_BIN)" TN_SEED_SIGNATURE_FORK_EPOCH=$(TN_SEED_SIGNATURE_FORK_EPOCH) TN_MULTI_WORKERS_FORK_EPOCH=$(TN_MULTI_WORKERS_FORK_EPOCH) TN_PREVRANDAO_FORK_EPOCH=$(TN_PREVRANDAO_FORK_EPOCH) cargo nextest run -p e2e-tests --run-ignored ignored-only --all-features ;

# run tests with coverage (using llvm-cov + nextest)
coverage:
Expand Down Expand Up @@ -249,8 +262,8 @@ revert-submodule:
# workspace tests that don't require faucet credentials
public-tests: build-e2e-bin
TN_BIN_PATH="$(E2E_BIN)" cargo nextest run --workspace --no-fail-fast ;
TN_BIN_PATH="$(E2E_BIN)" TN_SEED_SIGNATURE_FORK_EPOCH=$(TN_SEED_SIGNATURE_FORK_EPOCH) TN_MULTI_WORKERS_FORK_EPOCH=$(TN_MULTI_WORKERS_FORK_EPOCH) cargo nextest run -p e2e-tests --test it --run-ignored all test_epoch ;
TN_BIN_PATH="$(E2E_BIN)" TN_SEED_SIGNATURE_FORK_EPOCH=$(TN_SEED_SIGNATURE_FORK_EPOCH) TN_MULTI_WORKERS_FORK_EPOCH=$(TN_MULTI_WORKERS_FORK_EPOCH) cargo nextest run --run-ignored all test_restarts ;
TN_BIN_PATH="$(E2E_BIN)" TN_SEED_SIGNATURE_FORK_EPOCH=$(TN_SEED_SIGNATURE_FORK_EPOCH) TN_MULTI_WORKERS_FORK_EPOCH=$(TN_MULTI_WORKERS_FORK_EPOCH) TN_PREVRANDAO_FORK_EPOCH=$(TN_PREVRANDAO_FORK_EPOCH) cargo nextest run -p e2e-tests --test it --run-ignored all test_epoch ;
TN_BIN_PATH="$(E2E_BIN)" TN_SEED_SIGNATURE_FORK_EPOCH=$(TN_SEED_SIGNATURE_FORK_EPOCH) TN_MULTI_WORKERS_FORK_EPOCH=$(TN_MULTI_WORKERS_FORK_EPOCH) TN_PREVRANDAO_FORK_EPOCH=$(TN_PREVRANDAO_FORK_EPOCH) cargo nextest run --run-ignored all test_restarts ;

# local checks to ensure PR is ready
pr:
Expand Down
30 changes: 20 additions & 10 deletions crates/e2e-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,32 @@ impl TestBinary {
///
/// Pins every fork-epoch override on the child so spawned nodes run the fork points the
/// harness states rather than their build defaults (non-adiri builds are otherwise active
/// from genesis for both forks). With nothing in the harness environment each pin is
/// from genesis for every fork). With nothing in the harness environment each pin is
/// `u32::MAX`, holding that fork dormant: wire-identical to pre-fork mainnet for the seed
/// signature, the legacy single-worker layout for the committee worker list. A harness-level
/// value is forwarded verbatim so a fork-active lane can export
/// `TN_SEED_SIGNATURE_FORK_EPOCH=0` or `TN_MULTI_WORKERS_FORK_EPOCH=1`, and a single test
/// can still override a pin with its own later `env()` call. Only binaries built with
/// `tn-types/test-utils` (pulled in via `tn-storage/test-utils`, see `make build-e2e-bin`)
/// consult these variables; production binaries ignore them.
/// signature, the legacy single-worker layout for the committee worker list, the legacy
/// XOR mix hash for PREVRANDAO. A harness-level value is forwarded verbatim so a
/// fork-active lane can export `TN_SEED_SIGNATURE_FORK_EPOCH=0` or
/// `TN_MULTI_WORKERS_FORK_EPOCH=1`, and a single test can still override a pin with its
/// own later `env()` call. The seed and multi-workers forks arm independently; the seeded
/// PREVRANDAO derivation is the fail-closed conjunction of its own fork point and the
/// seed fork (`prevrandao_seed_active`), so its dormant pin here is what keeps a
/// seed-armed lane from arming it as a side effect, and a lane that wants it exports
/// both `TN_SEED_SIGNATURE_FORK_EPOCH=0` and `TN_PREVRANDAO_FORK_EPOCH=0`. Only binaries
/// built with `tn-types/test-utils` (pulled in via `tn-storage/test-utils`, see
/// `make build-e2e-bin`) consult these variables; production binaries ignore them.
pub fn command(&self) -> std::process::Command {
let mut command = match self {
TestBinary::Prebuilt(path) => std::process::Command::new(path),
TestBinary::Cargo(run) => run.command(),
};
// one loop rather than a block per variable so the two forks cannot drift apart in
// mechanism; they arm independently, so each is read and forwarded on its own
for var in ["TN_SEED_SIGNATURE_FORK_EPOCH", "TN_MULTI_WORKERS_FORK_EPOCH"] {
// one loop rather than a block per variable so the forks cannot drift apart in
// mechanism; each is read and forwarded on its own (see the conjunction note above
// for why PREVRANDAO's dormant default is load-bearing)
for var in [
"TN_SEED_SIGNATURE_FORK_EPOCH",
"TN_MULTI_WORKERS_FORK_EPOCH",
"TN_PREVRANDAO_FORK_EPOCH",
] {
let fork_epoch = std::env::var(var).unwrap_or_else(|_| u32::MAX.to_string());
command.env(var, fork_epoch);
}
Expand Down
32 changes: 20 additions & 12 deletions crates/engine/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2376,18 +2376,26 @@ async fn test_simple_basefee_penalty() -> eyre::Result<()> {
assert_ne!(block.number, 1);
}

// Independent oracle for the post-fork mix hash (active in this build): recompose
// the exact bytes by hand rather than trusting `ConsensusOutput::prev_randao` to
// check itself.
let expected_mix_hash = keccak256(
[
b"TN_PREVRANDAO_V1".as_slice(),
consensus_output.committee_shuffle_seed().as_slice(),
consensus_output.number().to_le_bytes().as_slice(),
0u64.to_le_bytes().as_slice(),
]
.concat(),
);
// Independent oracle for the mix hash: recompose by hand the exact bytes of
// whichever derivation this build's fork schedule selects, rather than trusting
// `ConsensusOutput::prev_randao` to check itself. The fixture leader sits at epoch
// 0, so adiri builds (dormant until `PREVRANDAO_FORK_EPOCH`) take the legacy XOR
// arm here and every other build takes the seeded arm from genesis.
let expected_mix_hash =
if tn_types::forks::prevrandao_seed_active(consensus_output.leader().epoch()) {
keccak256(
[
b"TN_PREVRANDAO_V1".as_slice(),
consensus_output.committee_shuffle_seed().as_slice(),
consensus_output.number().to_le_bytes().as_slice(),
0u64.to_le_bytes().as_slice(),
]
.concat(),
)
} else {
let output_digest: B256 = consensus_output_hash.into();
output_digest ^ batch_digest
};
assert_eq!(
expected_mix_hash,
consensus_output.prev_randao(0, batch_digest),
Expand Down
6 changes: 4 additions & 2 deletions crates/telcoin-network-cli/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,20 +120,22 @@ impl<Ext: clap::Args + fmt::Debug> NodeCommand<Ext> {
// Log the compiled fork schedule once per process start (#1086) so operators can diff it
// across the fleet before a fork epoch arrives. Adiri builds carry epoch-gated forks;
// every other build has the seed-signature (#1032) and multi-worker committee (#554)
// wire formats plus the region-aware committee shuffle (#1279) active from genesis.
// wire formats plus the seeded PREVRANDAO derivation (#1247) and the region-aware
// committee shuffle (#1279) active from genesis.
#[cfg(feature = "adiri")]
info!(
target: "cli",
consensus_registry_fork_epoch = tn_types::forks::CONSENSUS_REGISTRY_FORK_EPOCH,
seed_signature_fork_epoch = tn_types::forks::SEED_SIGNATURE_FORK_EPOCH,
multi_workers_fork_epoch = tn_types::forks::MULTI_WORKERS_FORK_EPOCH,
prevrandao_fork_epoch = tn_types::forks::PREVRANDAO_FORK_EPOCH,
region_shuffle_fork_epoch = tn_types::forks::REGION_SHUFFLE_FORK_EPOCH,
"fork schedule (adiri)"
);
#[cfg(not(feature = "adiri"))]
info!(
target: "cli",
"fork schedule: seed_signature, multi_workers, and region_shuffle active from genesis"
"fork schedule: seed_signature, multi_workers, prevrandao_seed, and region_shuffle active from genesis"
);

// Raise the fd limit of the process.
Expand Down
2 changes: 1 addition & 1 deletion crates/tn-reth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ TN repurposes several Ethereum header fields for protocol data. Assembly happens
|---|---|
| `nonce` | `((epoch as u64) << 32) \| round` of the leader certificate (`tn-types` `primary/header.rs`; decoded by `deconstruct_nonce`). Epoch = high 32 bits, round = low 32 bits. |
| `difficulty` | `batch_index << 16 \| worker_id`. **`worker_id` is the LOW 16 bits; `batch_index` occupies the upper bits** (`TNBlockExecutionCtx` docs and `context_for_next_block` in `src/evm/config.rs`). `first_batch()` exploits this: `difficulty < 65536` ⇔ `batch_index == 0`. `worker_id_from_header` (canonical in `tn-types` `gas_accumulator.rs`, re-exported from `src/snapshot.rs`) reads the low 16 bits. |
| `mix_hash` | Computed in `crates/engine/src/payload_builder.rs`: `output_digest ^ batch_digest` when the output has batches, plain `output_digest` otherwise. Exposed as `prevrandao` (EIP-4399). |
| `mix_hash` | Computed in `crates/engine/src/payload_builder.rs` via `ConsensusOutput::prev_randao`, fork-gated on `prevrandao_seed_active` for the committing leader's epoch (`PREVRANDAO_FORK_EPOCH` in `tn-types` `forks.rs`, conjoined fail-closed with the seed-signature fork). Before the fork: `output_digest ^ batch_digest` when the output has batches, plain `output_digest` otherwise. From the fork epoch: `keccak256("TN_PREVRANDAO_V1" \|\| epoch seed chain value \|\| consensus block number \|\| batch index)` (integers little-endian u64); the seed chain value advances every commit (one folded deterministic seed signature per commit), and the epoch's closing block publishes the closing commit's value as `extra_data`, a checkpoint that reproduces only that final commit's blocks. Exposed as `prevrandao` (EIP-4399). Post-fork the value cannot be ground by re-cutting the payload, but the committing leader computes every value before broadcasting and keeps one propose-or-withhold choice per commit . . . not unbiasable randomness on its own. |
| `extra_data` | Empty for a normal block. For an epoch-closing block: the 32-byte epoch-close randomness, which is epoch-gated on `seed_signature_active` (legacy keccak256 of the leader certificate's aggregate BLS signature before the fork epoch, epoch seed chain value as of the closing commit from the fork epoch on). The replay path (`context_for_block` in `src/evm/config.rs`) accepts only length 0 or 32 and errors on anything else. |
| `parent_beacon_block_root` | Digest of the `ConsensusHeader` that committed the executed transactions. Written to the EIP-4788 beacon-roots contract once per consensus output (only on the first batch, `apply_pre_execution_changes` in `src/evm/block.rs`). |
| `ommers_hash` | Digest of the executed `Batch`; `B256::ZERO` when the output carried no batches. |
Expand Down
4 changes: 4 additions & 0 deletions crates/tn-reth/src/evm/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,10 @@ where
withdrawals_root,
logs_bloom,
timestamp,
// fork-gated upstream (`ConsensusOutput::prev_randao`, #1247): legacy
// `output_digest ^ batch_digest` pre-fork, seed-chain keccak post-fork. The
// committing leader keeps one propose-or-withhold choice per commit either way,
// so this opcode alone is not unbiasable randomness.
mix_hash: evm_env.block_env.prevrandao().unwrap_or_default(),
nonce,
base_fee_per_gas: Some(evm_env.block_env.basefee()),
Expand Down
8 changes: 8 additions & 0 deletions crates/tn-reth/src/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ pub struct TNPayload {
/// The value comes from the worker's block.
pub gas_limit: u64,
/// The mix hash used for prev_randao.
///
/// Fork-gated by `prevrandao_seed_active` for the committing leader's epoch: the legacy
/// `output_digest ^ batch_digest` before the PREVRANDAO fork, the domain-separated
/// keccak over the epoch seed chain value, consensus block number, and batch index from
/// it (`ConsensusOutput::prev_randao`, #1247). Post-fork the value is immune to payload
/// grinding, but the committing leader still sees every value its commit will produce
/// before broadcasting and keeps one propose-or-withhold choice per commit; contracts
/// that need unbiasable randomness must not use `PREVRANDAO` alone.
pub mix_hash: B256,
/// Randomness digest carried only by the payload that closes the epoch.
///
Expand Down
58 changes: 58 additions & 0 deletions crates/types/src/forks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,16 @@ pub fn seed_signature_fork_epoch_override() -> Option<Epoch> {
/// history reproduces the same headers. Non-adiri builds carry no such history and are
/// active from genesis, exactly as with the seed-signature fork (like
/// [`SEED_SIGNATURE_FORK_EPOCH`], this constant does not exist there).
///
/// The full fork schedule is logged at startup so operators can diff it across the fleet
/// before an arming PR arrives; a compile-time constant that differs between binaries has
/// no other in-protocol detection.
///
/// Accepted residual (#1247): the seed chain closes payload grinding, but the committing
/// leader can still compute every `PREVRANDAO` its commit will produce before broadcasting
/// and withhold the proposal if it dislikes them . . . one propose-or-withhold choice per
/// commit. This fork promotes that bias into an opcode contracts can read; contracts that
/// need unbiasable randomness must not use `PREVRANDAO` alone.
#[cfg(feature = "adiri")]
pub const PREVRANDAO_FORK_EPOCH: Epoch = Epoch::MAX;

Expand Down Expand Up @@ -798,6 +808,54 @@ mod tests {
});
}

/// The fail-closed conjunction of [`prevrandao_seed_active`]: epochs where the seed
/// fork is dormant stay on the legacy arm regardless of the PREVRANDAO fork point.
///
/// Pins the seed fork to "never fires" and the PREVRANDAO fork point to "active from
/// genesis", the exact ordering the conjunct exists for. If the gate ever consulted the
/// fork point alone, every seed-dormant epoch would promote the forkable legacy
/// leader-aggregate seed into `PREVRANDAO`; this test observes that ordering directly
/// instead of relying on the compile-time `>=` assertion between the two constants.
#[cfg(feature = "test-utils")]
#[test]
fn prevrandao_stays_legacy_while_seed_fork_is_dormant() {
// Both overrides latch in process-wide `OnceLock`s, so pin them before the first
// read and fail loudly if another value latched first (nextest gives each test its
// own process). A silent mis-pin would make every assertion below vacuous.
std::env::set_var("TN_SEED_SIGNATURE_FORK_EPOCH", u32::MAX.to_string());
std::env::set_var("TN_PREVRANDAO_FORK_EPOCH", "0");
assert_eq!(
seed_signature_fork_epoch_override(),
Some(u32::MAX),
"TN_SEED_SIGNATURE_FORK_EPOCH latched to another value before this test pinned \
it; the override is OnceLock-latched, so run this test in its own process",
);
assert_eq!(
prevrandao_fork_epoch_override(),
Some(0),
"TN_PREVRANDAO_FORK_EPOCH latched to another value before this test pinned it; \
the override is OnceLock-latched, so run this test in its own process",
);
// The seed gate is `>=`, so `u32::MAX` itself fires it: the dormant grid stops one
// below. Every epoch on it has the fork point active (anti-vacuity: the conjunction
// is actually being exercised) yet must stay on the legacy arm.
[0, 1, 2, u32::MAX - 1].into_iter().for_each(|epoch| {
assert!(
prevrandao_fork_point_active(epoch),
"anti-vacuity: the pinned fork point must be active at epoch {epoch}",
);
assert!(
!seed_signature_active(epoch),
"the seed fork must be dormant at epoch {epoch} under the never-fires pin",
);
assert!(
!prevrandao_seed_active(epoch),
"seed-dormant epoch {epoch} must stay on the legacy arm even with the \
PREVRANDAO fork point active: the gate fails closed",
);
});
}

/// Pin the multi-workers gate to the rollout contract this build actually implements.
///
/// Carries the same asymmetry [`build_fork_gate_matches_this_builds_rollout_contract`] states
Expand Down
Loading
Loading