From dbff55b047d36eb6c4b03259242c94b10389e514 Mon Sep 17 00:00:00 2001 From: Sebastian Stammler Date: Fri, 7 Feb 2025 18:56:33 +0100 Subject: [PATCH] op-node/rollup/attributes: Add missing EIP1559Params consolidation checks (#14179) * op-node/rollup/attributes: Add missing EIP1559Params consolidation checks * all: use OptimismConfig in consolidation to translate zero attribs * TEST using default config if it cannot be fetched * op-node/node: add ChainOpConfig override and use it instead of a default config * Add ChainConfig's OptimismConfig into rollup.Config * Fix TestGetRollupConfig * revert passing OptimismConfig around it's now part of the rollup.Config --- op-chain-ops/genesis/config.go | 8 + op-e2e/e2eutils/setup.go | 8 +- op-e2e/system/e2esys/setup.go | 6 + op-node/chaincfg/chains_test.go | 19 +- op-node/node/node.go | 28 ++ .../rollup/attributes/engine_consolidate.go | 50 ++- .../attributes/engine_consolidate_test.go | 338 ++++++++++++------ op-node/rollup/superchain.go | 6 + op-node/rollup/types.go | 6 + 9 files changed, 358 insertions(+), 111 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 3539ca9b66b2..be37cb440304 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -976,6 +976,13 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *types.Header, l2GenesisBlockHa if d.SystemConfigProxy == (common.Address{}) { return nil, errors.New("SystemConfigProxy cannot be address(0)") } + + chainOpConfig := ¶ms.OptimismConfig{ + EIP1559Elasticity: d.EIP1559Elasticity, + EIP1559Denominator: d.EIP1559Denominator, + EIP1559DenominatorCanyon: &d.EIP1559DenominatorCanyon, + } + var altDA *rollup.AltDAConfig if d.UseAltDA { altDA = &rollup.AltDAConfig{ @@ -1021,6 +1028,7 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *types.Header, l2GenesisBlockHa InteropTime: d.InteropTime(l1StartTime), ProtocolVersionsAddress: d.ProtocolVersionsProxy, AltDAConfig: altDA, + ChainOpConfig: chainOpConfig, }, nil } diff --git a/op-e2e/e2eutils/setup.go b/op-e2e/e2eutils/setup.go index 6ce0fdc343a7..92fc08403c1d 100644 --- a/op-e2e/e2eutils/setup.go +++ b/op-e2e/e2eutils/setup.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" @@ -26,7 +27,7 @@ var testingJWTSecret = [32]byte{123} func WriteDefaultJWT(t TestingBase) string { // Sadly the geth node config cannot load JWT secret from memory, it has to be a file jwtPath := path.Join(t.TempDir(), "jwt_secret") - if err := os.WriteFile(jwtPath, []byte(hexutil.Encode(testingJWTSecret[:])), 0600); err != nil { + if err := os.WriteFile(jwtPath, []byte(hexutil.Encode(testingJWTSecret[:])), 0o600); err != nil { t.Fatalf("failed to prepare jwt file for geth: %v", err) } return jwtPath @@ -210,6 +211,11 @@ func Setup(t require.TestingT, deployParams *DeployParams, alloc *AllocParams) * IsthmusTime: deployConf.IsthmusTime(uint64(deployConf.L1GenesisBlockTimestamp)), InteropTime: deployConf.InteropTime(uint64(deployConf.L1GenesisBlockTimestamp)), AltDAConfig: pcfg, + ChainOpConfig: ¶ms.OptimismConfig{ + EIP1559Elasticity: deployConf.EIP1559Elasticity, + EIP1559Denominator: deployConf.EIP1559Denominator, + EIP1559DenominatorCanyon: &deployConf.EIP1559DenominatorCanyon, + }, } require.NoError(t, rollupCfg.Check()) diff --git a/op-e2e/system/e2esys/setup.go b/op-e2e/system/e2esys/setup.go index a5ffe01f793f..12f9af850788 100644 --- a/op-e2e/system/e2esys/setup.go +++ b/op-e2e/system/e2esys/setup.go @@ -245,6 +245,7 @@ func IsthmusSystemConfig(t *testing.T, isthmusTimeOffset *hexutil.Uint64, opts . cfg.DeployConfig.L2GenesisIsthmusTimeOffset = isthmusTimeOffset return cfg } + func writeDefaultJWT(t testing.TB) string { // Sadly the geth node config cannot load JWT secret from memory, it has to be a file jwtPath := path.Join(t.TempDir(), "jwt_secret") @@ -649,6 +650,11 @@ func (cfg SystemConfig) Start(t *testing.T, startOpts ...StartOption) (*System, InteropTime: cfg.DeployConfig.InteropTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)), ProtocolVersionsAddress: cfg.L1Deployments.ProtocolVersionsProxy, AltDAConfig: rollupAltDAConfig, + ChainOpConfig: ¶ms.OptimismConfig{ + EIP1559Elasticity: cfg.DeployConfig.EIP1559Elasticity, + EIP1559Denominator: cfg.DeployConfig.EIP1559Denominator, + EIP1559DenominatorCanyon: &cfg.DeployConfig.EIP1559DenominatorCanyon, + }, } } defaultConfig := makeRollupConfig() diff --git a/op-node/chaincfg/chains_test.go b/op-node/chaincfg/chains_test.go index fbb1247b9eb0..8f741b7427ce 100644 --- a/op-node/chaincfg/chains_test.go +++ b/op-node/chaincfg/chains_test.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" ) @@ -27,13 +28,20 @@ func TestGetRollupConfig(t *testing.T) { } for name, expectedCfg := range configsByName { - gotCfg, err := GetRollupConfig(name) - require.NoError(t, err) - - require.Equalf(t, expectedCfg, *gotCfg, "rollup-configs from superchain-registry must match for %v", name) + t.Run(name, func(t *testing.T) { + gotCfg, err := GetRollupConfig(name) + require.NoError(t, err) + require.Equalf(t, expectedCfg, *gotCfg, "rollup-configs from superchain-registry must match for %v", name) + }) } } +var defaultOpConfig = ¶ms.OptimismConfig{ + EIP1559Elasticity: 6, + EIP1559Denominator: 50, + EIP1559DenominatorCanyon: u64Ptr(250), +} + var mainnetCfg = rollup.Config{ Genesis: rollup.Genesis{ L1: eth.BlockID{ @@ -69,6 +77,7 @@ var mainnetCfg = rollup.Config{ GraniteTime: u64Ptr(1726070401), HoloceneTime: u64Ptr(1736445601), ProtocolVersionsAddress: common.HexToAddress("0x8062AbC286f5e7D9428a0Ccb9AbD71e50d93b935"), + ChainOpConfig: defaultOpConfig, } var sepoliaCfg = rollup.Config{ @@ -106,6 +115,7 @@ var sepoliaCfg = rollup.Config{ GraniteTime: u64Ptr(1723478400), HoloceneTime: u64Ptr(1732633200), ProtocolVersionsAddress: common.HexToAddress("0x79ADD5713B383DAa0a138d3C4780C7A1804a8090"), + ChainOpConfig: defaultOpConfig, } var sepoliaDev0Cfg = rollup.Config{ @@ -143,6 +153,7 @@ var sepoliaDev0Cfg = rollup.Config{ GraniteTime: u64Ptr(1723046400), HoloceneTime: u64Ptr(1731682800), ProtocolVersionsAddress: common.HexToAddress("0x252CbE9517F731C618961D890D534183822dcC8d"), + ChainOpConfig: defaultOpConfig, } func u64Ptr(v uint64) *uint64 { diff --git a/op-node/node/node.go b/op-node/node/node.go index bd5da091e3fe..1a1c8dea37fd 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "math/big" gosync "sync" "sync/atomic" "time" @@ -17,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum" gethevent "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-node/metrics" @@ -35,6 +37,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/oppprof" "github.com/ethereum-optimism/optimism/op-service/retry" "github.com/ethereum-optimism/optimism/op-service/sources" + "github.com/ethereum-optimism/optimism/op-service/superutil" ) var ErrAlreadyClosed = errors.New("node is already closed") @@ -434,11 +437,36 @@ func (n *OpNode) initL2(ctx context.Context, cfg *Config) error { } else { n.safeDB = safedb.Disabled } + + if cfg.Rollup.ChainOpConfig == nil { + chainCfg, err := loadOrFetchChainConfig(ctx, cfg.Rollup.L2ChainID, rpcClient) + if err != nil { + return fmt.Errorf("failed to load or fetch chain config for id %v: %w", cfg.Rollup.L2ChainID, err) + } + cfg.Rollup.ChainOpConfig = chainCfg.Optimism + } + n.l2Driver = driver.NewDriver(n.eventSys, n.eventDrain, &cfg.Driver, &cfg.Rollup, n.l2Source, n.l1Source, n.beacon, n, n, n.log, n.metrics, cfg.ConfigPersistence, n.safeDB, &cfg.Sync, sequencerConductor, altDA, managedMode) return nil } +func loadOrFetchChainConfig(ctx context.Context, id *big.Int, cl client.RPC) (*params.ChainConfig, error) { + if id.IsUint64() { + cfg, err := superutil.LoadOPStackChainConfigFromChainID(id.Uint64()) + if err == nil { + return cfg, nil + } + // ignore error, try to fetch chain config in full + } + // if not already recognized, then fetch the chain config manually + var config params.ChainConfig + if err := cl.CallContext(ctx, &config, "debug_chainConfig"); err != nil { + return nil, fmt.Errorf("fetching: %w", err) + } + return &config, nil +} + func (n *OpNode) initRPCServer(cfg *Config) error { server, err := newRPCServer(&cfg.RPC, &cfg.Rollup, n.l2Source.L2Client, n.l2Driver, n.safeDB, n.log, n.appVersion, n.metrics) if err != nil { diff --git a/op-node/rollup/attributes/engine_consolidate.go b/op-node/rollup/attributes/engine_consolidate.go index e28a4ce1911f..19a2b2aea8fe 100644 --- a/op-node/rollup/attributes/engine_consolidate.go +++ b/op-node/rollup/attributes/engine_consolidate.go @@ -7,8 +7,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" @@ -73,6 +75,10 @@ func AttributesMatchBlock(rollupCfg *rollup.Config, attrs *eth.PayloadAttributes if attrs.SuggestedFeeRecipient != block.FeeRecipient { return fmt.Errorf("fee recipient data does not match, expected %s but got %s", block.FeeRecipient, attrs.SuggestedFeeRecipient) } + if err := checkEIP1559ParamsMatch(rollupCfg.ChainOpConfig, attrs.EIP1559Params, block.ExtraData); err != nil { + return err + } + return nil } @@ -91,6 +97,49 @@ func checkParentBeaconBlockRootMatch(attrRoot, blockRoot *common.Hash) error { return nil } +func checkEIP1559ParamsMatch(opCfg *params.OptimismConfig, attrParams *eth.Bytes8, blockExtraData []byte) error { + // Note that we can assume that the attributes' eip1559params are non-nil iff Holocene is active + // according to the local rollup config. + if attrParams != nil { + params := (*attrParams)[:] + + // The validity checks are necessary because the Decode functions return 0,0 if the inputs are invalid. + // But 0,0 are valid values during derivation, namely, if the SystemConfig doesn't set the parameters yet, + // they are set to 0,0 and then the execution layer must translate them to the pre-Holocene constants. + if err := eip1559.ValidateHolocene1559Params(params); err != nil { + // This would be a critical error, because the attributes are generated by derivation and must be valid. + return fmt.Errorf("invalid attributes EIP1559 parameters: %w", err) + } else if err := eip1559.ValidateHoloceneExtraData(blockExtraData); err != nil { + // This can happen if the unsafe chain contains invalid (in particular, empty) extraData while Holocene + // is active. The extraData field of blocks from sequencer gossip isn't currently checked during import. + return fmt.Errorf("invalid block extraData: %w", err) + } + + ad, ae := eip1559.DecodeHolocene1559Params(params) + var translated bool + // Translate 0,0 to the pre-Holocene protocol constants, like the EL does too. + if ad == 0 { + // If attrParams are non-nil, Holocene, and so Canyon, must be active. + ad = *opCfg.EIP1559DenominatorCanyon + ae = opCfg.EIP1559Elasticity + translated = true + } + + bd, be := eip1559.DecodeHoloceneExtraData(blockExtraData) + if ad != bd || ae != be { + extraErr := "" + if translated { + extraErr = " (translated from 0,0)" + } + return fmt.Errorf("eip1559 parameters do not match, attributes: %d, %d%s, block: %d, %d", ad, ae, extraErr, bd, be) + } + } else if len(blockExtraData) > 0 { + // When deriving pre-Holocene blocks, the extraData must be empty. + return fmt.Errorf("nil EIP1559Params in attributes but non-nil extraData in block: %v", blockExtraData) + } + return nil +} + // checkWithdrawals checks if the withdrawals list and withdrawalsRoot are as expected in the attributes and block, // based on the active hard fork. func checkWithdrawals(rollupCfg *rollup.Config, attrs *eth.PayloadAttributes, block *eth.ExecutionPayload) error { @@ -122,7 +171,6 @@ func checkWithdrawals(rollupCfg *rollup.Config, attrs *eth.PayloadAttributes, bl // bedrock: the withdrawals list should be nil if attrWithdrawals != nil { return fmt.Errorf("%w: got %d", ErrBedrockMustHaveEmptyWithdrawals, len(*attrWithdrawals)) - } } diff --git a/op-node/rollup/attributes/engine_consolidate_test.go b/op-node/rollup/attributes/engine_consolidate_test.go index 54efa72d60c3..ad1af4ce65b2 100644 --- a/op-node/rollup/attributes/engine_consolidate_test.go +++ b/op-node/rollup/attributes/engine_consolidate_test.go @@ -4,7 +4,9 @@ import ( "math/rand" // nosemgrep "testing" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/common" @@ -18,23 +20,39 @@ import ( "github.com/ethereum-optimism/optimism/op-service/testutils" ) -var ( - validParentHash = common.HexToHash("0x123") - validTimestamp = eth.Uint64Quantity(123) - validParentBeaconRoot = common.HexToHash("0x456") - validPrevRandao = eth.Bytes32(common.HexToHash("0x789")) - validGasLimit = eth.Uint64Quantity(1000) - validFeeRecipient = predeploys.SequencerFeeVaultAddr -) +var defaultOpConfig = ¶ms.OptimismConfig{ + EIP1559Elasticity: 6, + EIP1559Denominator: 50, + EIP1559DenominatorCanyon: ptr(uint64(250)), +} + +func ptr[T any](t T) *T { + return &t +} -type args struct { +type matchArgs struct { envelope *eth.ExecutionPayloadEnvelope attrs *eth.PayloadAttributes parentHash common.Hash } -func ecotoneArgs() args { - return args{ +func holoceneArgs() matchArgs { + var ( + validParentHash = common.HexToHash("0x123") + validTimestamp = eth.Uint64Quantity(50) + validParentBeaconRoot = common.HexToHash("0x456") + validPrevRandao = eth.Bytes32(common.HexToHash("0x789")) + validGasLimit = eth.Uint64Quantity(1000) + validFeeRecipient = predeploys.SequencerFeeVaultAddr + validTx = testutils.RandomLegacyTxNotProtected(rand.New(rand.NewSource(42))) + validTxData, _ = validTx.MarshalBinary() + + validHoloceneExtraData = eth.BytesMax32(eip1559.EncodeHoloceneExtraData( + *defaultOpConfig.EIP1559DenominatorCanyon, defaultOpConfig.EIP1559Elasticity)) + validHoloceneEIP1559Params = new(eth.Bytes8) + ) + + return matchArgs{ envelope: ð.ExecutionPayloadEnvelope{ ParentBeaconBlockRoot: &validParentBeaconRoot, ExecutionPayload: ð.ExecutionPayload{ @@ -42,8 +60,10 @@ func ecotoneArgs() args { Timestamp: validTimestamp, PrevRandao: validPrevRandao, GasLimit: validGasLimit, - Withdrawals: nil, + Transactions: []eth.Data{validTxData}, + Withdrawals: &types.Withdrawals{}, FeeRecipient: validFeeRecipient, + ExtraData: validHoloceneExtraData, }, }, attrs: ð.PayloadAttributes{ @@ -51,207 +71,248 @@ func ecotoneArgs() args { PrevRandao: validPrevRandao, GasLimit: &validGasLimit, ParentBeaconBlockRoot: &validParentBeaconRoot, - Withdrawals: nil, + Transactions: []eth.Data{validTxData}, + Withdrawals: &types.Withdrawals{}, SuggestedFeeRecipient: validFeeRecipient, + EIP1559Params: validHoloceneEIP1559Params, }, parentHash: validParentHash, } } -func canyonArgs() args { +func ecotoneArgs() matchArgs { + args := holoceneArgs() + args.attrs.EIP1559Params = nil + args.envelope.ExecutionPayload.ExtraData = nil + return args +} + +func canyonArgs() matchArgs { args := ecotoneArgs() args.attrs.ParentBeaconBlockRoot = nil args.envelope.ParentBeaconBlockRoot = nil return args } -func bedrockArgs() args { - args := ecotoneArgs() +func bedrockArgs() matchArgs { + args := canyonArgs() args.attrs.Withdrawals = nil args.envelope.ExecutionPayload.Withdrawals = nil return args } -func ecotoneNoParentBeaconBlockRoot() args { +func ecotoneNoParentBeaconBlockRoot() matchArgs { args := ecotoneArgs() args.envelope.ParentBeaconBlockRoot = nil return args } -func ecotoneUnexpectedParentBeaconBlockRoot() args { +func ecotoneUnexpectedParentBeaconBlockRoot() matchArgs { args := ecotoneArgs() args.attrs.ParentBeaconBlockRoot = nil return args } -func ecotoneMismatchParentBeaconBlockRoot() args { +func ecotoneMismatchParentBeaconBlockRoot() matchArgs { args := ecotoneArgs() h := common.HexToHash("0xabc") args.attrs.ParentBeaconBlockRoot = &h return args } -func ecotoneMismatchParentBeaconBlockRootPtr() args { +func ecotoneMismatchParentBeaconBlockRootPtr() matchArgs { args := ecotoneArgs() cpy := *args.attrs.ParentBeaconBlockRoot args.attrs.ParentBeaconBlockRoot = &cpy return args } -func ecotoneNilParentBeaconBlockRoots() args { +func ecotoneNilParentBeaconBlockRoots() matchArgs { args := ecotoneArgs() args.attrs.ParentBeaconBlockRoot = nil args.envelope.ParentBeaconBlockRoot = nil return args } -func mismatchedParentHashArgs() args { +func mismatchedParentHashArgs() matchArgs { args := ecotoneArgs() args.parentHash = common.HexToHash("0xabc") return args } -func createMismatchedPrevRandao() args { +func createMismatchedPrevRandao() matchArgs { args := ecotoneArgs() args.attrs.PrevRandao = eth.Bytes32(common.HexToHash("0xabc")) return args } -func createMismatchedGasLimit() args { +func createMismatchedGasLimit() matchArgs { args := ecotoneArgs() val := eth.Uint64Quantity(2000) args.attrs.GasLimit = &val return args } -func createNilGasLimit() args { +func createNilGasLimit() matchArgs { args := ecotoneArgs() args.attrs.GasLimit = nil return args } -func createMismatchedTimestamp() args { +func createMismatchedTimestamp() matchArgs { args := ecotoneArgs() - val := eth.Uint64Quantity(2000) - args.attrs.Timestamp = val + args.attrs.Timestamp++ return args } -func createMismatchedFeeRecipient() args { +func createMismatchedTransactions() matchArgs { + args := ecotoneArgs() + args.attrs.Transactions = append(args.attrs.Transactions, args.attrs.Transactions[0]) + return args +} + +func createMismatchedFeeRecipient() matchArgs { args := ecotoneArgs() args.attrs.SuggestedFeeRecipient = common.Address{0xde, 0xad} return args } -func TestAttributesMatch(t *testing.T) { - canyonTimeInFuture := uint64(100) - canyonTimeInPast := uint64(0) - isthmusTimeInFuture := uint64(250) +func createMismatchedEIP1559Params() matchArgs { + args := holoceneArgs() + args.attrs.EIP1559Params[0]++ // so denominator is != 0 + return args +} - rollupCfgPreCanyonChecks := &rollup.Config{CanyonTime: &canyonTimeInFuture} - rollupCfgPreIsthmusChecks := &rollup.Config{CanyonTime: &canyonTimeInPast, IsthmusTime: &isthmusTimeInFuture} +func TestAttributesMatch(t *testing.T) { + // default valid timestamp is 50 + pastTime := uint64(0) + futureTime := uint64(100) - rollupCfg := &rollup.Config{} + rollupCfgPreCanyon := &rollup.Config{CanyonTime: &futureTime, ChainOpConfig: defaultOpConfig} + rollupCfgPreIsthmus := &rollup.Config{CanyonTime: &pastTime, IsthmusTime: &futureTime, ChainOpConfig: defaultOpConfig} tests := []struct { - shouldMatch bool - args args - rollupCfg *rollup.Config - desc string + args matchArgs + rollupCfg *rollup.Config + err string + desc string }{ { - shouldMatch: true, - args: ecotoneArgs(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "ecotoneArgs", + args: bedrockArgs(), + rollupCfg: rollupCfgPreCanyon, + desc: "validBedrockArgs", + }, + { + args: bedrockArgs(), + rollupCfg: rollupCfgPreIsthmus, + err: ErrCanyonMustHaveWithdrawals.Error() + ": block", + desc: "bedrockArgsPostCanyon", }, { - shouldMatch: true, - args: canyonArgs(), - rollupCfg: rollupCfgPreIsthmusChecks, - desc: "canyonArgs", + args: canyonArgs(), + rollupCfg: rollupCfgPreIsthmus, + desc: "validCanyonArgs", }, { - shouldMatch: true, - args: bedrockArgs(), - rollupCfg: rollupCfgPreIsthmusChecks, - desc: "bedrockArgs", + args: ecotoneArgs(), + rollupCfg: rollupCfgPreIsthmus, + desc: "validEcotoneArgs", }, { - shouldMatch: false, - args: mismatchedParentHashArgs(), - rollupCfg: rollupCfgPreIsthmusChecks, - desc: "mismatchedParentHashArgs", + args: holoceneArgs(), + rollupCfg: rollupCfgPreIsthmus, + desc: "validholoceneArgs", }, { - shouldMatch: false, - args: ecotoneNoParentBeaconBlockRoot(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "ecotoneNoParentBeaconBlockRoot", + args: mismatchedParentHashArgs(), + rollupCfg: rollupCfgPreIsthmus, + err: "parent hash field does not match", + desc: "mismatchedParentHashArgs", }, { - shouldMatch: false, - args: ecotoneUnexpectedParentBeaconBlockRoot(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "ecotoneUnexpectedParentBeaconBlockRoot", + args: createMismatchedTimestamp(), + rollupCfg: rollupCfgPreIsthmus, + err: "timestamp field does not match", + desc: "createMismatchedTimestamp", }, { - shouldMatch: false, - args: ecotoneMismatchParentBeaconBlockRoot(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "ecotoneMismatchParentBeaconBlockRoot", + args: createMismatchedPrevRandao(), + rollupCfg: rollupCfgPreIsthmus, + err: "random field does not match", + desc: "createMismatchedPrevRandao", }, { - shouldMatch: true, - args: ecotoneMismatchParentBeaconBlockRootPtr(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "ecotoneMismatchParentBeaconBlockRootPtr", + args: createMismatchedTransactions(), + rollupCfg: rollupCfgPreIsthmus, + err: "transaction count does not match", + desc: "createMismatchedTransactions", }, { - shouldMatch: true, - args: ecotoneNilParentBeaconBlockRoots(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "ecotoneNilParentBeaconBlockRoots", + args: ecotoneNoParentBeaconBlockRoot(), + rollupCfg: rollupCfgPreIsthmus, + err: "expected non-nil parent beacon block root", + desc: "ecotoneNoParentBeaconBlockRoot", }, { - shouldMatch: false, - args: createMismatchedPrevRandao(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "createMismatchedPrevRandao", + args: ecotoneUnexpectedParentBeaconBlockRoot(), + rollupCfg: rollupCfgPreIsthmus, + err: "expected nil parent beacon block root but got non-nil", + desc: "ecotoneUnexpectedParentBeaconBlockRoot", }, { - shouldMatch: false, - args: createMismatchedGasLimit(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "createMismatchedGasLimit", + args: ecotoneMismatchParentBeaconBlockRoot(), + rollupCfg: rollupCfgPreIsthmus, + err: "parent beacon block root does not match", + desc: "ecotoneMismatchParentBeaconBlockRoot", }, { - shouldMatch: false, - args: createNilGasLimit(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "createNilGasLimit", + args: ecotoneMismatchParentBeaconBlockRootPtr(), + rollupCfg: rollupCfgPreIsthmus, + desc: "ecotoneMismatchParentBeaconBlockRootPtr", }, { - shouldMatch: false, - args: createMismatchedTimestamp(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "createMismatchedTimestamp", + args: ecotoneNilParentBeaconBlockRoots(), + rollupCfg: rollupCfgPreIsthmus, + desc: "ecotoneNilParentBeaconBlockRoots", }, { - shouldMatch: false, - args: createMismatchedFeeRecipient(), - rollupCfg: rollupCfgPreCanyonChecks, - desc: "createMismatchedFeeRecipient", + args: createMismatchedGasLimit(), + rollupCfg: rollupCfgPreIsthmus, + err: "gas limit does not match", + desc: "createMismatchedGasLimit", + }, + { + args: createNilGasLimit(), + rollupCfg: rollupCfgPreIsthmus, + err: "expected gaslimit in attributes to not be nil", + desc: "createNilGasLimit", + }, + { + args: createMismatchedFeeRecipient(), + rollupCfg: rollupCfgPreIsthmus, + err: "fee recipient data does not match", + desc: "createMismatchedFeeRecipient", + }, + { + args: createMismatchedEIP1559Params(), + rollupCfg: rollupCfgPreIsthmus, + err: "eip1559 parameters do not match", + desc: "createMismatchedEIP1559Params", }, } for _, test := range tests { - err := AttributesMatchBlock(rollupCfg, test.args.attrs, test.args.parentHash, test.args.envelope, testlog.Logger(t, log.LevelInfo)) - if test.shouldMatch { - require.NoError(t, err, "fail: %s", test.desc) - } else { - require.Error(t, err, "fail: %s", test.desc) - } + t.Run(test.desc, func(t *testing.T) { + err := AttributesMatchBlock(test.rollupCfg, + test.args.attrs, test.args.parentHash, test.args.envelope, + testlog.Logger(t, log.LevelInfo), + ) + if test.err == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, test.err) + } + }) } } @@ -263,9 +324,9 @@ func TestWithdrawalsMatch(t *testing.T) { emptyWithdrawals := make(types.Withdrawals, 0) - rollupCfgPreCanyonChecks := &rollup.Config{CanyonTime: &canyonTimeInFuture} - rollupCfgPreIsthmusChecks := &rollup.Config{CanyonTime: &canyonTimeInPast, IsthmusTime: &isthmusTimeInFuture} - rollupCfgPostIsthmusChecks := &rollup.Config{CanyonTime: &canyonTimeInPast, IsthmusTime: &isthmusTimeInPast} + rollupCfgPreCanyonChecks := &rollup.Config{CanyonTime: &canyonTimeInFuture, ChainOpConfig: defaultOpConfig} + rollupCfgPreIsthmusChecks := &rollup.Config{CanyonTime: &canyonTimeInPast, IsthmusTime: &isthmusTimeInFuture, ChainOpConfig: defaultOpConfig} + rollupCfgPostIsthmusChecks := &rollup.Config{CanyonTime: &canyonTimeInPast, IsthmusTime: &isthmusTimeInPast, ChainOpConfig: defaultOpConfig} tests := []struct { cfg *rollup.Config @@ -427,6 +488,73 @@ func TestWithdrawalsMatch(t *testing.T) { } } +func TestCheckEIP1559ParamsMatch(t *testing.T) { + params := eth.Bytes8{1, 2, 3, 4, 5, 6, 7, 8} + paramsAlt := eth.Bytes8{1, 2, 3, 4, 5, 6, 7, 9} + paramsInvalid := eth.Bytes8{0, 0, 0, 0, 5, 6, 7, 8} + defaultExtraData := eth.BytesMax32(eip1559.EncodeHoloceneExtraData( + *defaultOpConfig.EIP1559DenominatorCanyon, defaultOpConfig.EIP1559Elasticity)) + + for _, test := range []struct { + desc string + attrParams *eth.Bytes8 + blockExtraData eth.BytesMax32 + err string + }{ + { + desc: "match-empty", + }, + { + desc: "match-zero-attrs", + attrParams: new(eth.Bytes8), + blockExtraData: defaultExtraData, + }, + { + desc: "match-non-zero", + attrParams: ¶ms, + blockExtraData: append(eth.BytesMax32{0}, params[:]...), + }, + { + desc: "err-both-zero", + attrParams: new(eth.Bytes8), + blockExtraData: make(eth.BytesMax32, 9), + err: "eip1559 parameters do not match, attributes: 250, 6 (translated from 0,0), block: 0, 0", + }, + { + desc: "err-invalid-params", + attrParams: ¶msInvalid, + blockExtraData: append(eth.BytesMax32{0}, paramsInvalid[:]...), + err: "invalid attributes EIP1559 parameters: holocene params cannot have a 0 denominator unless elasticity is also 0", + }, + { + desc: "err-invalid-extra", + attrParams: ¶ms, + blockExtraData: append(eth.BytesMax32{42}, params[:]...), + err: "invalid block extraData: holocene extraData should have 0 version byte, got 42", + }, + { + desc: "err-no-match", + attrParams: ¶msAlt, + blockExtraData: append(eth.BytesMax32{0}, params[:]...), + err: "eip1559 parameters do not match", + }, + { + desc: "err-non-nil-extra", + blockExtraData: make(eth.BytesMax32, 9), + err: "nil EIP1559Params in attributes but non-nil extraData in block", + }, + } { + t.Run(test.desc, func(t *testing.T) { + err := checkEIP1559ParamsMatch(defaultOpConfig, test.attrParams, test.blockExtraData) + if test.err == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, test.err) + } + }) + } +} + func TestGetMissingTxnHashes(t *testing.T) { depositTxs := make([]*types.Transaction, 5) diff --git a/op-node/rollup/superchain.go b/op-node/rollup/superchain.go index cbae732e7c5f..b73c70e96fae 100644 --- a/op-node/rollup/superchain.go +++ b/op-node/rollup/superchain.go @@ -24,6 +24,11 @@ func LoadOPStackRollupConfig(chainID uint64) (*Config, error) { if err != nil { return nil, fmt.Errorf("unable to retrieve chain %d config: %w", chainID, err) } + chOpConfig := ¶ms.OptimismConfig{ + EIP1559Elasticity: chConfig.Optimism.EIP1559Elasticity, + EIP1559Denominator: chConfig.Optimism.EIP1559Denominator, + EIP1559DenominatorCanyon: chConfig.Optimism.EIP1559DenominatorCanyon, + } superConfig, err := superchain.GetSuperchain(chain.Network) if err != nil { @@ -88,6 +93,7 @@ func LoadOPStackRollupConfig(chainID uint64) (*Config, error) { DepositContractAddress: *addrs.OptimismPortalProxy, L1SystemConfigAddress: *addrs.SystemConfigProxy, AltDAConfig: altDA, + ChainOpConfig: chOpConfig, } cfg.ProtocolVersionsAddress = superConfig.ProtocolVersionsAddr diff --git a/op-node/rollup/types.go b/op-node/rollup/types.go index d28b38309012..7b144e09daab 100644 --- a/op-node/rollup/types.go +++ b/op-node/rollup/types.go @@ -143,6 +143,12 @@ type Config struct { // AltDAConfig. We are in the process of migrating to the AltDAConfig from these legacy top level values AltDAConfig *AltDAConfig `json:"alt_da,omitempty"` + + // ChainOpConfig is the OptimismConfig of the execution layer ChainConfig. + // It is used during safe chain consolidation to translate zero SystemConfig EIP1559 + // parameters to the protocol values, like the execution layer does. + // If missing, it is loaded by the op-node from the embedded superchain config at startup. + ChainOpConfig *params.OptimismConfig `json:"chain_op_config,omitempty"` } // ValidateL1Config checks L1 config variables for errors.