ethereum-optimism / ethereum-optimism/optimism

alloy-op-evm: Canyon create2-deployer activation check hardcodes a 2-second block time

Open
#22,684 0 comments 0 reactions 0 assignees View on GitHub
A-kona A-op-reth R-execution
Dominant language
Go
Stars
6.5k
Forks
4k
Avg merge
2d 15h
Merged PRs (30d)
145

Description

## Summary

[`ensure_create2_deployer`](https://github.com/ethereum-optimism/optimism/blob/68b009443f67cf830990e913bcbde8536cb15327/rust/alloy-op-evm/src/block/canyon.rs#L18-L36) detects the Canyon activation block — the block whose irregular state transition force-deploys the create2 deployer — with a hardcoded 2-second block time:

```rust
// If the canyon hardfork is active at the current timestamp, and it was not active at the
// previous block timestamp (heuristically, block time is not perfectly constant at 2s), and the
// chain is an optimism chain, then we need to force-deploy the create2 deployer contract.
if chain_spec.is_canyon_active_at_timestamp(timestamp) &&
!chain_spec.is_canyon_active_at_timestamp(timestamp.saturating_sub(2))
```

The comment's premise is wrong for OP: L2 block timestamps are exactly `genesis_l2_time + n * block_time`, so block time *is* constant — it is just not always 2. `block_time` is a per-chain rollup-config value and chains run at 1, 2, 5 and 10 seconds today.

The same crate already has the correct primitive one module over: [`OpHardforks::is_no_user_tx_activation_block(parent_timestamp, block_timestamp)`](https://github.com/ethereum-optimism/optimism/blob/68b009443f67cf830990e913bcbde8536cb15327/rust/alloy-op-hardforks/src/lib.rs#L237-L247).

## Precise analysis

Let `T_c` = `canyon_time`, `b` = block time, `ts` = block timestamp, parent timestamp = `ts - b`.

* The activation block is the first block with `ts >= T_c`, so its timestamp lies in `[T_c, T_c + b)`.
* **Current predicate** fires iff `ts ∈ [T_c, T_c + 2)`.
* **Correct (parent-relative) predicate** fires iff `ts ∈ [T_c, T_c + b)` — exactly the activation block, for any `b`.

| block time | behaviour today |
| --- | --- |
| `b == 2` | identical to the parent-relative form. No change. |
| `b == 1` | fires on the activation block **and the one after it**. The force-deploy is idempotent (fixed code + codehash on a non-empty account), so there is no state-root divergence — just a redundant account touch and changeset entry. |
| `b > 2` | **can miss the activation block entirely.** |

The `b > 2` miss is real, and it is worth being precise about when: it needs the activation block's timestamp to land at or after `T_c + 2`, which happens iff `T_c` is *not* aligned to the chain's block grid. Concretely with `b = 4`, blocks at `…, 998, 1002, …` and `T_c = 1000`: the activation block is `1002`, and `1002 - 2 = 1000 >= T_c`, so Canyon reads as *already active* at the probe point and the predicate is false. The create2 deployer is never force-deployed — a silent divergence from the specified irregular state transition.

Conversely, when `T_c` **is** grid-aligned the activation block's timestamp equals `T_c` exactly, which is inside `[T_c, T_c + 2)` for every `b`, and the current predicate is accidentally right. That is why nothing is broken today.

## Live-chain status: latent, not active

From `superchain-registry` @ `08d6a44910d75e28a5ee1c7c047d3bdd0bbdbdd2`:

* `block_time` distribution across chain configs: **43×2, 8×1, 1×5** (`mainnet/swan.toml`), **1×10** (`sepolia/cyber.toml`).
* Every chain with `block_time != 2` has `canyon_time <= genesis.l2_time` — Canyon is active from genesis, the deployer is in the genesis alloc, and the predicate never fires either way.
* Every chain with a post-genesis `canyon_time` (op, zora, mode, lyra, orderly, lisk-sep, metal-sep, …) has `block_time = 2` **and** `(canyon_time - l2_time) % 2 == 0`.

So no registry chain is affected. The exposure is new or custom chains, devnets and test harnesses that pick a non-2s block time with a post-genesis Canyon — and the crate is also published standalone as `alloy-op-evm`.

## Go side (mentioned, not to be fixed here)

op-geth's [`EnsureCreate2Deployer`](https://github.com/ethereum-optimism/op-geth/blob/v1.101702.3-rc.6/consensus/misc/create2deployer.go#L34-L39) (the version pinned by `go.mod`'s `replace` at `v1.101702.3-rc.6`) uses **exact equality**:

```go
if !c.IsOptimism() || c.CanyonTime == nil || *c.CanyonTime != timestamp {
return
}
```

That is block-time-independent but has the mirror-image bug: for an unaligned `canyon_time`, op-geth never force-deploys at all — while alloy-op-evm does, if the activation block happens to land in `[T_c, T_c+2)`. So the Go/Rust divergence for that config already exists today, in the opposite direction.

`op-node` does not execute blocks, and `op-core` carries no create2-deployer logic — only the predeploy address at [`op-core/predeploys/addresses.go#L37`](https://github.com/ethereum-optimism/optimism/blob/68b009443f67cf830990e913bcbde8536cb15327/op-core/predeploys/addresses.go#L37). op-geth is the only Go site.

## Fix

Key the check off the fork timestamp itself — `op_fork_activation(Canyon).as_timestamp() == Some(timestamp)` — which is exactly what op-geth does. That removes the block-time assumption, needs no parent header, and keeps every execution path (block building, `engine_newPayload`, sync re-execution, kona's proof executor) on one rule.

The parent-relative form is the *spec-level* definition of "activation block", but adopting it unilaterally in Rust would be the wrong move: on a chain with an unaligned `canyon_time` op-geth never force-deploys, so the canonical chain state has no create2 deployer, and deploying it in Rust would produce a state root the real chain never had. The residual hole — Canyon's consensus rules activate but the create2 deployer never lands — is shared by Go and Rust and would have to be closed on both sides together, if at all. It is unreachable on every registered chain.

Fixed in [#22691](https://github.com/ethereum-optimism/optimism/pull/22691).

🤖 *Co-created with Claude Fable 5*

Contributor guide

Open the contributing guide

Research direction

Start in rust/alloy-op-evm/src/block/canyon.rs and review the existing ensure_create2_deployer check alongside alloy-op-hardforks/src/lib.rs. Compare the Rust behavior with op-geth’s EnsureCreate2Deployer in the pinned Go dependency, then review PR #22691; done means the activation check is consistent across the execution paths described in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, rust
Domain
blockchain
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.