diff --git a/op-node/rollup/celo_espresso_params.go b/op-node/rollup/celo_espresso_params.go new file mode 100644 index 00000000000..2eebf462109 --- /dev/null +++ b/op-node/rollup/celo_espresso_params.go @@ -0,0 +1,97 @@ +package rollup + +import ( + _ "embed" + "encoding/json" + "errors" + "fmt" + "strconv" + + "github.com/ethereum/go-ethereum/common" +) + +// celoEspressoParamsJSON is the canonical source of truth for the Espresso batch-authentication +// parameters of the known Celo chains, keyed by L2 chain ID. The Espresso parameters are +// consensus-critical: espresso_time switches batch authorization from sender-based to +// BatchAuthenticator event-based, so op-node and the fault-proof program must agree on them or +// the two derivation pipelines produce divergent outputs from the same inputs. +// +// The file is mirrored byte-for-byte in celo-kona at crates/kona/proof/src/celo_espresso_params.json, +// where a unit test asserts the program-baked constants (CELO_MAINNET_ESPRESSO / +// CELO_SEPOLIA_ESPRESSO / CELO_CHAOS_ESPRESSO in crates/kona/proof/src/boot.rs) match it. When +// scheduling Espresso on a chain, update the file in both repositories together with the baked +// constants in celo-kona. +// +//go:embed celo_espresso_params.json +var celoEspressoParamsJSON []byte + +// celoEspressoParams is one chain's entry in celo_espresso_params.json. Both fields are nil for +// a chain on which Espresso is not (yet) scheduled. +type celoEspressoParams struct { + EspressoTime *uint64 `json:"espresso_time"` + BatchAuthenticatorAddress *common.Address `json:"batch_authenticator_address"` +} + +// celoEspressoParamsByChainID is celo_espresso_params.json parsed at package load. The embedded +// file is part of the build, so any malformed or internally inconsistent content is a programming +// error and panics rather than being reported at config-load time. +var celoEspressoParamsByChainID = func() map[uint64]celoEspressoParams { + var raw map[string]celoEspressoParams + if err := json.Unmarshal(celoEspressoParamsJSON, &raw); err != nil { + panic(fmt.Errorf("invalid embedded celo_espresso_params.json: %w", err)) + } + byChainID := make(map[uint64]celoEspressoParams, len(raw)) + for key, params := range raw { + chainID, err := strconv.ParseUint(key, 10, 64) + if err != nil { + panic(fmt.Errorf("invalid chain ID %q in embedded celo_espresso_params.json: %w", key, err)) + } + if (params.EspressoTime == nil) != (params.BatchAuthenticatorAddress == nil) { + panic(fmt.Errorf("chain %d in embedded celo_espresso_params.json must set espresso_time and batch_authenticator_address together", chainID)) + } + if params.BatchAuthenticatorAddress != nil && *params.BatchAuthenticatorAddress == (common.Address{}) { + panic(fmt.Errorf("chain %d in embedded celo_espresso_params.json has a zero batch_authenticator_address", chainID)) + } + byChainID[chainID] = params + } + return byChainID +}() + +// ErrCeloEspressoParamsMismatch is returned by Check when the loaded rollup config carries +// Espresso parameters that differ from the canonical celo_espresso_params.json entry for the +// chain. The fault-proof program derives the known Celo chains with the canonical values baked +// in, so a diverging op-node config would derive a different chain than the proofs attest to. +var ErrCeloEspressoParamsMismatch = errors.New("espresso params diverge from the canonical celo_espresso_params.json entry for this chain") + +// checkCeloEspressoParams validates the config's Espresso parameters against the canonical +// per-chain values embedded from celo_espresso_params.json. A no-op for chains without a +// canonical entry (devnets, non-Celo chains), which keep whatever the config carries. +func (cfg *Config) checkCeloEspressoParams() error { + if cfg.L2ChainID == nil || !cfg.L2ChainID.IsUint64() { + return nil + } + canonical, ok := celoEspressoParamsByChainID[cfg.L2ChainID.Uint64()] + if !ok { + return nil + } + fmtTime := func(t *uint64) string { + if t == nil { + return "unset" + } + return strconv.FormatUint(*t, 10) + } + if (cfg.EspressoTime == nil) != (canonical.EspressoTime == nil) || + (cfg.EspressoTime != nil && *cfg.EspressoTime != *canonical.EspressoTime) { + return fmt.Errorf("%w: espresso_time is %s, canonical is %s", + ErrCeloEspressoParamsMismatch, fmtTime(cfg.EspressoTime), fmtTime(canonical.EspressoTime)) + } + canonicalAddr := common.Address{} // canonical entries carry no address while Espresso is unscheduled + if canonical.BatchAuthenticatorAddress != nil { + canonicalAddr = *canonical.BatchAuthenticatorAddress + } + if cfg.BatchAuthenticatorAddress != canonicalAddr { + return fmt.Errorf("%w: batch_authenticator_address is %s, canonical is %s", + ErrCeloEspressoParamsMismatch, cfg.BatchAuthenticatorAddress, canonicalAddr) + } + return nil +} diff --git a/op-node/rollup/celo_espresso_params.json b/op-node/rollup/celo_espresso_params.json new file mode 100644 index 00000000000..990bcce8236 --- /dev/null +++ b/op-node/rollup/celo_espresso_params.json @@ -0,0 +1,14 @@ +{ + "42220": { + "espresso_time": null, + "batch_authenticator_address": null + }, + "11142220": { + "espresso_time": null, + "batch_authenticator_address": null + }, + "11162320": { + "espresso_time": 1782910800, + "batch_authenticator_address": "0xb4B5343d9635b05cA4FbdB09BB4929E21A1A8B37" + } +} diff --git a/op-node/rollup/celo_espresso_params_test.go b/op-node/rollup/celo_espresso_params_test.go new file mode 100644 index 00000000000..8e6b6edec8f --- /dev/null +++ b/op-node/rollup/celo_espresso_params_test.go @@ -0,0 +1,91 @@ +package rollup + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" +) + +// chaosBatchAuthenticator is the canonical BatchAuthenticator address for Celo Chaos, duplicated +// here as a tripwire: changing celo_espresso_params.json must be a deliberate act that also +// updates this test (and the baked constants in celo-kona's crates/kona/proof/src/boot.rs). +var chaosBatchAuthenticator = common.HexToAddress("0xb4B5343d9635b05cA4FbdB09BB4929E21A1A8B37") + +// TestCeloEspressoParams_CanonicalFile pins the parsed content of the embedded +// celo_espresso_params.json: exactly the three known Celo chains, with Espresso scheduled only +// on Chaos. +func TestCeloEspressoParams_CanonicalFile(t *testing.T) { + require.Len(t, celoEspressoParamsByChainID, 3) + + for _, chainID := range []uint64{42220, 11142220} { + params, ok := celoEspressoParamsByChainID[chainID] + require.True(t, ok, "missing canonical entry for chain %d", chainID) + require.Nil(t, params.EspressoTime, "Espresso must be unscheduled on chain %d", chainID) + require.Nil(t, params.BatchAuthenticatorAddress, "no authenticator expected on chain %d", chainID) + } + + chaos, ok := celoEspressoParamsByChainID[11162320] + require.True(t, ok, "missing canonical entry for Celo Chaos") + require.NotNil(t, chaos.EspressoTime) + require.EqualValues(t, 1782910800, *chaos.EspressoTime) + require.NotNil(t, chaos.BatchAuthenticatorAddress) + require.Equal(t, chaosBatchAuthenticator, *chaos.BatchAuthenticatorAddress) +} + +// TestConfig_Check_CeloEspressoParams verifies that Check rejects Espresso parameters that +// diverge from the canonical celo_espresso_params.json entry for the known Celo chain IDs, and +// leaves other chains alone. +func TestConfig_Check_CeloEspressoParams(t *testing.T) { + // configFor builds an otherwise-valid config for the given chain with the given Espresso + // parameters. Ecotone (and its predecessor forks) activate at genesis, as on the real Celo + // chains, so the espresso-after-ecotone rule is satisfied whenever espressoTime is set. + configFor := func(chainID uint64, espressoTime *uint64, authenticator common.Address) *Config { + cfg := randConfig() + zero := uint64(0) + cfg.RegolithTime = &zero + cfg.CanyonTime = &zero + cfg.DeltaTime = &zero + cfg.EcotoneTime = &zero + cfg.L2ChainID = new(big.Int).SetUint64(chainID) + cfg.EspressoTime = espressoTime + cfg.BatchAuthenticatorAddress = authenticator + return cfg + } + + chaosTime := uint64(1782910800) + + // The canonical parameters pass for each known chain. + require.NoError(t, configFor(42220, nil, common.Address{}).Check()) + require.NoError(t, configFor(11142220, nil, common.Address{}).Check()) + require.NoError(t, configFor(11162320, &chaosTime, chaosBatchAuthenticator).Check()) + + // Chaos: missing espresso_time, wrong espresso_time, or wrong authenticator all diverge. + err := configFor(11162320, nil, chaosBatchAuthenticator).Check() + require.ErrorIs(t, err, ErrCeloEspressoParamsMismatch) + wrongTime := chaosTime + 1 + err = configFor(11162320, &wrongTime, chaosBatchAuthenticator).Check() + require.ErrorIs(t, err, ErrCeloEspressoParamsMismatch) + err = configFor(11162320, &chaosTime, common.Address{0x01}).Check() + require.ErrorIs(t, err, ErrCeloEspressoParamsMismatch) + + // Mainnet/Sepolia: scheduling Espresso (or carrying an authenticator) without updating the + // canonical file is rejected. + err = configFor(42220, &chaosTime, chaosBatchAuthenticator).Check() + require.ErrorIs(t, err, ErrCeloEspressoParamsMismatch) + err = configFor(11142220, nil, chaosBatchAuthenticator).Check() + require.ErrorIs(t, err, ErrCeloEspressoParamsMismatch) + + // A chain without a canonical entry keeps whatever the config carries. + require.NoError(t, configFor(901, &chaosTime, chaosBatchAuthenticator).Check()) + require.NoError(t, configFor(901, nil, common.Address{}).Check()) + + // A non-uint64 L2 chain ID cannot have a canonical entry and is not rejected here. + huge, ok := new(big.Int).SetString("340282366920938463463374607431768211456", 10) // 2^128 + require.True(t, ok) + cfg := configFor(901, nil, common.Address{}) + cfg.L2ChainID = huge + require.NoError(t, cfg.checkCeloEspressoParams()) +} diff --git a/op-node/rollup/types.go b/op-node/rollup/types.go index 47dd86cb1cb..02cdbe36e54 100644 --- a/op-node/rollup/types.go +++ b/op-node/rollup/types.go @@ -386,6 +386,12 @@ func (cfg *Config) Check() error { return err } + // Espresso parameters are consensus-critical and, for the known Celo chains, baked into the + // fault-proof program (celo-kona). Reject a config that diverges from the canonical values, + // so op-node and the proof program cannot silently derive different chains. + if err := cfg.checkCeloEspressoParams(); err != nil { + return err + } if cfg.EspressoTime != nil { // When Espresso is enabled, batches must be authenticated via BatchInfoAuthenticated // events emitted by the BatchAuthenticator contract, so a non-zero authenticator