Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions op-node/rollup/celo_espresso_params.go
Original file line number Diff line number Diff line change
@@ -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
}
14 changes: 14 additions & 0 deletions op-node/rollup/celo_espresso_params.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
91 changes: 91 additions & 0 deletions op-node/rollup/celo_espresso_params_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
6 changes: 6 additions & 0 deletions op-node/rollup/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading