diff --git a/bedrock-devnet/devnet/__init__.py b/bedrock-devnet/devnet/__init__.py index 70243dc790c8..7bf5c6a2595c 100644 --- a/bedrock-devnet/devnet/__init__.py +++ b/bedrock-devnet/devnet/__init__.py @@ -24,7 +24,7 @@ log = logging.getLogger() # Global constants -FORKS = ["delta", "ecotone", "fjord", "granite", "holocene"] +FORKS = ["delta", "ecotone", "fjord", "granite", "holocene", "isthmus"] # Global environment variables DEVNET_NO_BUILD = os.getenv('DEVNET_NO_BUILD') == "true" diff --git a/go.mod b/go.mod index d077cf6a8bb0..3cb7fd9e2462 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/crate-crypto/go-kzg-4844 v1.0.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 - github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240910145426-b3905c89e8ac + github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20241113154227-e72c6311f6e7 github.com/ethereum/go-ethereum v1.14.11 github.com/fsnotify/fsnotify v1.8.0 github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb @@ -250,7 +250,7 @@ require ( rsc.io/tmplfunc v0.0.3 // indirect ) -replace github.com/ethereum/go-ethereum v1.14.11 => github.com/ethereum-optimism/op-geth v1.101411.1-rc.6 +replace github.com/ethereum/go-ethereum v1.14.11 => ../op-geth //replace github.com/ethereum/go-ethereum => ../go-ethereum diff --git a/go.sum b/go.sum index d9f532807675..ec59071a6480 100644 --- a/go.sum +++ b/go.sum @@ -187,10 +187,8 @@ github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/u github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z8veEq5ZO3DfIhZ7xgRP9WTc= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs= -github.com/ethereum-optimism/op-geth v1.101411.1-rc.6 h1:VvUBIVFbnU9486CWHa9Js5XYY3o6OsdQcI8gE3XjCDE= -github.com/ethereum-optimism/op-geth v1.101411.1-rc.6/go.mod h1:7S4pp8KHBmEmKkRjL1BPOc6jY9hW+64YeMUjR3RVLw4= -github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240910145426-b3905c89e8ac h1:hCIrLuOPV3FJfMDvXeOhCC3uQNvFoMIIlkT2mN2cfeg= -github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240910145426-b3905c89e8ac/go.mod h1:XaVXL9jg8BcyOeugECgIUGa9Y3DjYJj71RHmb5qon6M= +github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20241113154227-e72c6311f6e7 h1:Mbgsp5T52F2pEULHccLr4NtnT6cKnJgabpAPlTfPxrk= +github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20241113154227-e72c6311f6e7/go.mod h1:9feO8jcL5OZ1tvRjEfNAHz4Aggvd6373l+ZxmZZAyZs= github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A= diff --git a/op-batcher/batcher/channel_builder_test.go b/op-batcher/batcher/channel_builder_test.go index 957f9ae59739..e378d2cea0b0 100644 --- a/op-batcher/batcher/channel_builder_test.go +++ b/op-batcher/batcher/channel_builder_test.go @@ -15,6 +15,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" "github.com/stretchr/testify/require" @@ -62,7 +63,7 @@ func newMiniL2BlockWithNumberParentAndL1Information(numTx int, l2Number *big.Int Difficulty: common.Big0, Number: big.NewInt(l1Number), Time: blockTime, - }, nil, nil, trie.NewStackTrie(nil)) + }, nil, nil, trie.NewStackTrie(nil), ¶ms.ChainConfig{}) l1InfoTx, err := derive.L1InfoDeposit(defaultTestRollupConfig, eth.SystemConfig{}, 0, eth.BlockToInfo(l1Block), blockTime) if err != nil { panic(err) @@ -77,7 +78,7 @@ func newMiniL2BlockWithNumberParentAndL1Information(numTx int, l2Number *big.Int return types.NewBlock(&types.Header{ Number: l2Number, ParentHash: parent, - }, &types.Body{Transactions: txs}, nil, trie.NewStackTrie(nil)) + }, &types.Body{Transactions: txs}, nil, trie.NewStackTrie(nil), ¶ms.ChainConfig{}) } // addTooManyBlocks adds blocks to the channel until it hits an error, diff --git a/op-batcher/batcher/channel_manager_test.go b/op-batcher/batcher/channel_manager_test.go index 8dcb0745c164..fa89f1e7332b 100644 --- a/op-batcher/batcher/channel_manager_test.go +++ b/op-batcher/batcher/channel_manager_test.go @@ -18,6 +18,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" ) @@ -69,19 +70,19 @@ func ChannelManagerReturnsErrReorg(t *testing.T, batchType uint) { a := types.NewBlock(&types.Header{ Number: big.NewInt(0), - }, nil, nil, nil) + }, nil, nil, nil, ¶ms.ChainConfig{}) b := types.NewBlock(&types.Header{ Number: big.NewInt(1), ParentHash: a.Hash(), - }, nil, nil, nil) + }, nil, nil, nil, ¶ms.ChainConfig{}) c := types.NewBlock(&types.Header{ Number: big.NewInt(2), ParentHash: b.Hash(), - }, nil, nil, nil) + }, nil, nil, nil, ¶ms.ChainConfig{}) x := types.NewBlock(&types.Header{ Number: big.NewInt(2), ParentHash: common.Hash{0xff}, - }, nil, nil, nil) + }, nil, nil, nil, ¶ms.ChainConfig{}) require.NoError(t, m.AddL2Block(a)) require.NoError(t, m.AddL2Block(b)) @@ -171,7 +172,7 @@ func ChannelManager_Clear(t *testing.T, batchType uint) { b := types.NewBlock(&types.Header{ Number: big.NewInt(1), ParentHash: a.Hash(), - }, nil, nil, nil) + }, nil, nil, nil, ¶ms.ChainConfig{}) require.NoError(m.AddL2Block(b)) require.Len(m.blocks, 1) require.Equal(b.Hash(), m.tip) diff --git a/op-chain-ops/cmd/check-derivation/main.go b/op-chain-ops/cmd/check-derivation/main.go index 499b128f8767..4769da9ac57b 100644 --- a/op-chain-ops/cmd/check-derivation/main.go +++ b/op-chain-ops/cmd/check-derivation/main.go @@ -114,7 +114,8 @@ func newClientsFromContext(cliCtx *cli.Context) (*ethclient.Client, *sources.Eth MethodResetDuration: time.Minute, } cl := ethclient.NewClient(clients.L2RpcClient) - ethCl, err := sources.NewEthClient(client.NewBaseRPCClient(clients.L2RpcClient), log.Root(), nil, ðClCfg) + l2RpcChecker := sources.NewL2RPCChecker() + ethCl, err := sources.NewEthClient(client.NewBaseRPCClient(clients.L2RpcClient), log.Root(), nil, ðClCfg, l2RpcChecker) if err != nil { return nil, nil, err } diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 7ed0a6e2f682..26a845dc2f73 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -345,6 +345,9 @@ type UpgradeScheduleDeployConfig struct { // L2GenesisHoloceneTimeOffset is the number of seconds after genesis block that the Holocene hard fork activates. // Set it to 0 to activate at genesis. Nil to disable Holocene. L2GenesisHoloceneTimeOffset *hexutil.Uint64 `json:"l2GenesisHoloceneTimeOffset,omitempty"` + // L2GenesisIsthmusTimeOffset is the number of seconds after genesis block that the Isthmus hard fork activates. + // Set it to 0 to activate at genesis. Nil to disable Isthmus. + L2GenesisIsthmusTimeOffset *hexutil.Uint64 `json:"l2GenesisIsthmusTimeOffset,omitempty"` // L2GenesisInteropTimeOffset is the number of seconds after genesis block that the Interop hard fork activates. // Set it to 0 to activate at genesis. Nil to disable Interop. L2GenesisInteropTimeOffset *hexutil.Uint64 `json:"l2GenesisInteropTimeOffset,omitempty"` @@ -385,6 +388,8 @@ func (d *UpgradeScheduleDeployConfig) ForkTimeOffset(fork rollup.ForkName) *uint return (*uint64)(d.L2GenesisGraniteTimeOffset) case rollup.Holocene: return (*uint64)(d.L2GenesisHoloceneTimeOffset) + case rollup.Isthmus: + return (*uint64)(d.L2GenesisIsthmusTimeOffset) case rollup.Interop: return (*uint64)(d.L2GenesisInteropTimeOffset) default: @@ -408,6 +413,8 @@ func (d *UpgradeScheduleDeployConfig) SetForkTimeOffset(fork rollup.ForkName, of d.L2GenesisGraniteTimeOffset = (*hexutil.Uint64)(offset) case rollup.Holocene: d.L2GenesisHoloceneTimeOffset = (*hexutil.Uint64)(offset) + case rollup.Isthmus: + d.L2GenesisIsthmusTimeOffset = (*hexutil.Uint64)(offset) case rollup.Interop: d.L2GenesisInteropTimeOffset = (*hexutil.Uint64)(offset) default: @@ -472,6 +479,10 @@ func (d *UpgradeScheduleDeployConfig) HoloceneTime(genesisTime uint64) *uint64 { return offsetToUpgradeTime(d.L2GenesisHoloceneTimeOffset, genesisTime) } +func (d *UpgradeScheduleDeployConfig) IsthmusTime(genesisTime uint64) *uint64 { + return offsetToUpgradeTime(d.L2GenesisIsthmusTimeOffset, genesisTime) +} + func (d *UpgradeScheduleDeployConfig) InteropTime(genesisTime uint64) *uint64 { return offsetToUpgradeTime(d.L2GenesisInteropTimeOffset, genesisTime) } @@ -504,6 +515,7 @@ func (d *UpgradeScheduleDeployConfig) forks() []Fork { {L2GenesisTimeOffset: d.L2GenesisFjordTimeOffset, Name: string(L2AllocsFjord)}, {L2GenesisTimeOffset: d.L2GenesisGraniteTimeOffset, Name: string(L2AllocsGranite)}, {L2GenesisTimeOffset: d.L2GenesisHoloceneTimeOffset, Name: string(L2AllocsHolocene)}, + {L2GenesisTimeOffset: d.L2GenesisIsthmusTimeOffset, Name: string(L2AllocsIsthmus)}, } } @@ -1014,6 +1026,7 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *types.Header, l2GenesisBlockHa FjordTime: d.FjordTime(l1StartTime), GraniteTime: d.GraniteTime(l1StartTime), HoloceneTime: d.HoloceneTime(l1StartTime), + IsthmusTime: d.IsthmusTime(l1StartTime), InteropTime: d.InteropTime(l1StartTime), ProtocolVersionsAddress: d.ProtocolVersionsProxy, AltDAConfig: altDA, diff --git a/op-chain-ops/genesis/genesis.go b/op-chain-ops/genesis/genesis.go index c852fa0fc100..56bb0453cf8d 100644 --- a/op-chain-ops/genesis/genesis.go +++ b/op-chain-ops/genesis/genesis.go @@ -71,6 +71,7 @@ func NewL2Genesis(config *DeployConfig, l1StartHeader *types.Header) (*core.Gene FjordTime: config.FjordTime(l1StartTime), GraniteTime: config.GraniteTime(l1StartTime), HoloceneTime: config.HoloceneTime(l1StartTime), + IsthmusTime: config.IsthmusTime(l1StartTime), InteropTime: config.InteropTime(l1StartTime), Optimism: ¶ms.OptimismConfig{ EIP1559Denominator: eip1559Denom, diff --git a/op-chain-ops/genesis/layer_two.go b/op-chain-ops/genesis/layer_two.go index b1c0922de1eb..50a70c42a2b9 100644 --- a/op-chain-ops/genesis/layer_two.go +++ b/op-chain-ops/genesis/layer_two.go @@ -27,6 +27,7 @@ const ( L2AllocsFjord L2AllocsMode = "fjord" L2AllocsGranite L2AllocsMode = "granite" L2AllocsHolocene L2AllocsMode = "holocene" + L2AllocsIsthmus L2AllocsMode = "isthmus" ) var ( diff --git a/op-conductor/conductor/service.go b/op-conductor/conductor/service.go index cccba2c76ac9..d709865a900e 100644 --- a/op-conductor/conductor/service.go +++ b/op-conductor/conductor/service.go @@ -129,8 +129,9 @@ func (c *OpConductor) initSequencerControl(ctx context.Context) error { return errors.Wrap(err, "failed to create geth rpc client") } execCfg := sources.L2ClientDefaultConfig(&c.cfg.RollupCfg, true) + l2RpcChecker := sources.NewL2RPCChecker() // TODO: Add metrics tracer here. tracked by https://github.com/ethereum-optimism/protocol-quest/issues/45 - exec, err := sources.NewEthClient(ec, c.log, nil, &execCfg.EthClientConfig) + exec, err := sources.NewEthClient(ec, c.log, nil, &execCfg.EthClientConfig, l2RpcChecker) if err != nil { return errors.Wrap(err, "failed to create geth client") } diff --git a/op-e2e/actions/helpers/env.go b/op-e2e/actions/helpers/env.go index 77911aa80bf5..60d2e79c8062 100644 --- a/op-e2e/actions/helpers/env.go +++ b/op-e2e/actions/helpers/env.go @@ -52,7 +52,7 @@ func WithActiveGenesisFork(fork rollup.ForkName) EnvOpt { // DefaultFork specifies the default fork to use when setting up the action test environment. // Currently manually set to Holocene. // Replace with `var DefaultFork = func() rollup.ForkName { return rollup.AllForks[len(rollup.AllForks)-1] }()` after Interop launch. -const DefaultFork = rollup.Holocene +const DefaultFork = rollup.Isthmus // SetupEnv sets up a default action test environment. If no fork is specified, the default fork as // specified by the package variable [defaultFork] is used. diff --git a/op-e2e/actions/helpers/l1_miner.go b/op-e2e/actions/helpers/l1_miner.go index 108c11d3e166..523c13f2387a 100644 --- a/op-e2e/actions/helpers/l1_miner.go +++ b/op-e2e/actions/helpers/l1_miner.go @@ -218,7 +218,7 @@ func (s *L1Miner) ActL1EndBlock(t Testing) *types.Block { withdrawals = make([]*types.Withdrawal, 0) } - block := types.NewBlock(s.l1BuildingHeader, &types.Body{Transactions: s.L1Transactions, Withdrawals: withdrawals}, s.l1Receipts, trie.NewStackTrie(nil)) + block := types.NewBlock(s.l1BuildingHeader, &types.Body{Transactions: s.L1Transactions, Withdrawals: withdrawals}, s.l1Receipts, trie.NewStackTrie(nil), s.l1Cfg.Config) if s.l1Cfg.Config.IsCancun(s.l1BuildingHeader.Number, s.l1BuildingHeader.Time) { parent := s.l1Chain.GetHeaderByHash(s.l1BuildingHeader.ParentHash) var ( diff --git a/op-e2e/actions/helpers/l2_sequencer.go b/op-e2e/actions/helpers/l2_sequencer.go index daf267839581..20eecf3d5fae 100644 --- a/op-e2e/actions/helpers/l2_sequencer.go +++ b/op-e2e/actions/helpers/l2_sequencer.go @@ -206,6 +206,13 @@ func (s *L2Sequencer) ActBuildL2ToTime(t Testing, target uint64) { } } +func (s *L2Sequencer) ActBuildL2ToCanyon(t Testing) { + require.NotNil(t, s.RollupCfg.CanyonTime, "cannot activate CanyonTime when it is not scheduled") + for s.L2Unsafe().Time < *s.RollupCfg.CanyonTime { + s.ActL2EmptyBlock(t) + } +} + func (s *L2Sequencer) ActBuildL2ToEcotone(t Testing) { require.NotNil(t, s.RollupCfg.EcotoneTime, "cannot activate Ecotone when it is not scheduled") for s.L2Unsafe().Time < *s.RollupCfg.EcotoneTime { @@ -233,3 +240,10 @@ func (s *L2Sequencer) ActBuildL2ToHolocene(t Testing) { s.ActL2EmptyBlock(t) } } + +func (s *L2Sequencer) ActBuildL2ToIsthmus(t Testing) { + require.NotNil(t, s.RollupCfg.IsthmusTime, "cannot activate IsthmusTime when it is not scheduled") + for s.L2Unsafe().Time < *s.RollupCfg.IsthmusTime { + s.ActL2EmptyBlock(t) + } +} diff --git a/op-e2e/actions/helpers/user_test.go b/op-e2e/actions/helpers/user_test.go index 6ac7e598c593..00b27dea1fe4 100644 --- a/op-e2e/actions/helpers/user_test.go +++ b/op-e2e/actions/helpers/user_test.go @@ -27,6 +27,7 @@ type hardforkScheduledTest struct { fjordTime *hexutil.Uint64 graniteTime *hexutil.Uint64 holoceneTime *hexutil.Uint64 + isthmusTime *hexutil.Uint64 runToFork string allocType config.AllocType } @@ -41,6 +42,8 @@ func (tc *hardforkScheduledTest) GetFork(fork string) *uint64 { func (tc *hardforkScheduledTest) fork(fork string) **hexutil.Uint64 { switch fork { + case "isthmus": + return &tc.isthmusTime case "holocene": return &tc.holoceneTime case "granite": @@ -88,6 +91,7 @@ func testCrossLayerUser(t *testing.T, allocType config.AllocType) { "fjord", "granite", "holocene", + "isthmus", } for i, fork := range forks { i := i @@ -146,6 +150,7 @@ func runCrossLayerUserTest(gt *testing.T, test hardforkScheduledTest) { dp.DeployConfig.L2GenesisFjordTimeOffset = test.fjordTime dp.DeployConfig.L2GenesisGraniteTimeOffset = test.graniteTime dp.DeployConfig.L2GenesisHoloceneTimeOffset = test.holoceneTime + dp.DeployConfig.L2GenesisIsthmusTimeOffset = test.isthmusTime if test.canyonTime != nil { require.Zero(t, uint64(*test.canyonTime)%uint64(dp.DeployConfig.L2BlockTime), "canyon fork must be aligned") diff --git a/op-e2e/actions/upgrades/helpers/config.go b/op-e2e/actions/upgrades/helpers/config.go index c09d0be48b6c..01d11fc1a30a 100644 --- a/op-e2e/actions/upgrades/helpers/config.go +++ b/op-e2e/actions/upgrades/helpers/config.go @@ -43,4 +43,13 @@ func ApplyDeltaTimeOffset(dp *e2eutils.DeployParams, deltaTimeOffset *hexutil.Ui dp.DeployConfig.L2GenesisHoloceneTimeOffset = deltaTimeOffset } } + + // configure Isthmus to not be before Delta accidentally + if dp.DeployConfig.L2GenesisIsthmusTimeOffset != nil { + if deltaTimeOffset == nil { + dp.DeployConfig.L2GenesisIsthmusTimeOffset = nil + } else if *dp.DeployConfig.L2GenesisIsthmusTimeOffset < *deltaTimeOffset { + dp.DeployConfig.L2GenesisIsthmusTimeOffset = deltaTimeOffset + } + } } diff --git a/op-e2e/config/init.go b/op-e2e/config/init.go index 9419c1277060..93ae683e4c75 100644 --- a/op-e2e/config/init.go +++ b/op-e2e/config/init.go @@ -185,6 +185,7 @@ func initAllocType(root string, allocType AllocType) { } l2Alloc[mode] = allocs } + mustL2Allocs(genesis.L2AllocsIsthmus) mustL2Allocs(genesis.L2AllocsHolocene) mustL2Allocs(genesis.L2AllocsGranite) mustL2Allocs(genesis.L2AllocsFjord) diff --git a/op-e2e/e2eutils/setup.go b/op-e2e/e2eutils/setup.go index dbc9ecea25b9..f35a334dad31 100644 --- a/op-e2e/e2eutils/setup.go +++ b/op-e2e/e2eutils/setup.go @@ -107,6 +107,9 @@ func Ether(v uint64) *big.Int { } func GetL2AllocsMode(dc *genesis.DeployConfig, t uint64) genesis.L2AllocsMode { + if fork := dc.IsthmusTime(t); fork != nil && *fork <= 0 { + return genesis.L2AllocsIsthmus + } if fork := dc.HoloceneTime(t); fork != nil && *fork <= 0 { return genesis.L2AllocsHolocene } @@ -205,6 +208,7 @@ func Setup(t require.TestingT, deployParams *DeployParams, alloc *AllocParams) * FjordTime: deployConf.FjordTime(uint64(deployConf.L1GenesisBlockTimestamp)), GraniteTime: deployConf.GraniteTime(uint64(deployConf.L1GenesisBlockTimestamp)), HoloceneTime: deployConf.HoloceneTime(uint64(deployConf.L1GenesisBlockTimestamp)), + IsthmusTime: deployConf.IsthmusTime(uint64(deployConf.L1GenesisBlockTimestamp)), InteropTime: deployConf.InteropTime(uint64(deployConf.L1GenesisBlockTimestamp)), AltDAConfig: pcfg, } @@ -235,6 +239,7 @@ func SystemConfigFromDeployConfig(deployConfig *genesis.DeployConfig) eth.System } func ApplyDeployConfigForks(deployConfig *genesis.DeployConfig) { + isIsthmus := os.Getenv("OP_E2E_USE_ISTHMUS") == "true" isHolocene := os.Getenv("OP_E2E_USE_HOLOCENE") == "true" isGranite := isHolocene || os.Getenv("OP_E2E_USE_GRANITE") == "true" isFjord := isGranite || os.Getenv("OP_E2E_USE_FJORD") == "true" @@ -255,6 +260,9 @@ func ApplyDeployConfigForks(deployConfig *genesis.DeployConfig) { if isHolocene { deployConfig.L2GenesisHoloceneTimeOffset = new(hexutil.Uint64) } + if isIsthmus { + deployConfig.L2GenesisIsthmusTimeOffset = new(hexutil.Uint64) + } // Canyon and lower is activated by default deployConfig.L2GenesisCanyonTimeOffset = new(hexutil.Uint64) deployConfig.L2GenesisRegolithTimeOffset = new(hexutil.Uint64) diff --git a/op-e2e/opgeth/op_geth.go b/op-e2e/opgeth/op_geth.go index 28a358b6c175..babc4bfebf49 100644 --- a/op-e2e/opgeth/op_geth.go +++ b/op-e2e/opgeth/op_geth.go @@ -18,8 +18,10 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/predeploys" "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" @@ -150,6 +152,25 @@ func (d *OpGeth) AddL2Block(ctx context.Context, txs ...*types.Transaction) (*et return nil, errors.New("required transactions were not included") } + // if we are at Isthmus, set the withdrawalsRoot in the execution payload to the storage root of the message passer contract + if d.L2ChainConfig.IsIsthmus(uint64(payload.Timestamp)) { + var getProofResponse *eth.AccountResult + rpcClient := d.l2Engine.RPC + err := rpcClient.CallContext(ctx, &getProofResponse, "eth_getProof", predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, payload.BlockHash.String()) + if err != nil { + return nil, err + } + if getProofResponse == nil { + return nil, ethereum.NotFound + } + // output, err := d.l2Engine.OutputV0AtBlock(ctx, payload.BlockHash) + // if err != nil { + // return nil, fmt.Errorf("output at block: %w", err) + // } + storageHash := getProofResponse.StorageHash + payload.WithdrawalsRoot = &storageHash + } + status, err := d.l2Engine.NewPayload(ctx, payload, envelope.ParentBeaconBlockRoot) if err != nil { return nil, fmt.Errorf("new payload: %w", err) diff --git a/op-node/chaincfg/chains_test.go b/op-node/chaincfg/chains_test.go index 7499aa4ba3ee..bd9956253cd5 100644 --- a/op-node/chaincfg/chains_test.go +++ b/op-node/chaincfg/chains_test.go @@ -89,22 +89,22 @@ var sepoliaCfg = rollup.Config{ GasLimit: 30000000, }, }, - BlockTime: 2, - MaxSequencerDrift: 600, - SeqWindowSize: 3600, - ChannelTimeoutBedrock: 300, - L1ChainID: big.NewInt(11155111), - L2ChainID: big.NewInt(11155420), - BatchInboxAddress: common.HexToAddress("0xff00000000000000000000000000000011155420"), - DepositContractAddress: common.HexToAddress("0x16fc5058f25648194471939df75cf27a2fdc48bc"), - L1SystemConfigAddress: common.HexToAddress("0x034edd2a225f7f429a63e0f1d2084b9e0a93b538"), - RegolithTime: u64Ptr(0), - CanyonTime: u64Ptr(1699981200), - DeltaTime: u64Ptr(1703203200), - EcotoneTime: u64Ptr(1708534800), - FjordTime: u64Ptr(1716998400), - GraniteTime: u64Ptr(1723478400), - // HoloceneTime: TBD + BlockTime: 2, + MaxSequencerDrift: 600, + SeqWindowSize: 3600, + ChannelTimeoutBedrock: 300, + L1ChainID: big.NewInt(11155111), + L2ChainID: big.NewInt(11155420), + BatchInboxAddress: common.HexToAddress("0xff00000000000000000000000000000011155420"), + DepositContractAddress: common.HexToAddress("0x16fc5058f25648194471939df75cf27a2fdc48bc"), + L1SystemConfigAddress: common.HexToAddress("0x034edd2a225f7f429a63e0f1d2084b9e0a93b538"), + RegolithTime: u64Ptr(0), + CanyonTime: u64Ptr(1699981200), + DeltaTime: u64Ptr(1703203200), + EcotoneTime: u64Ptr(1708534800), + FjordTime: u64Ptr(1716998400), + GraniteTime: u64Ptr(1723478400), + HoloceneTime: u64Ptr(1732633200), ProtocolVersionsAddress: common.HexToAddress("0x79ADD5713B383DAa0a138d3C4780C7A1804a8090"), } @@ -126,22 +126,22 @@ var sepoliaDev0Cfg = rollup.Config{ GasLimit: 30000000, }, }, - BlockTime: 2, - MaxSequencerDrift: 600, - SeqWindowSize: 3600, - ChannelTimeoutBedrock: 300, - L1ChainID: big.NewInt(11155111), - L2ChainID: big.NewInt(11155421), - BatchInboxAddress: common.HexToAddress("0xff00000000000000000000000000000011155421"), - DepositContractAddress: common.HexToAddress("0x76114bd29dFcC7a9892240D317E6c7C2A281Ffc6"), - L1SystemConfigAddress: common.HexToAddress("0xa6b72407e2dc9EBF84b839B69A24C88929cf20F7"), - RegolithTime: u64Ptr(0), - CanyonTime: u64Ptr(0), - DeltaTime: u64Ptr(0), - EcotoneTime: u64Ptr(1706634000), - FjordTime: u64Ptr(1715961600), - GraniteTime: u64Ptr(1723046400), - // HoloceneTime: TBD + BlockTime: 2, + MaxSequencerDrift: 600, + SeqWindowSize: 3600, + ChannelTimeoutBedrock: 300, + L1ChainID: big.NewInt(11155111), + L2ChainID: big.NewInt(11155421), + BatchInboxAddress: common.HexToAddress("0xff00000000000000000000000000000011155421"), + DepositContractAddress: common.HexToAddress("0x76114bd29dFcC7a9892240D317E6c7C2A281Ffc6"), + L1SystemConfigAddress: common.HexToAddress("0xa6b72407e2dc9EBF84b839B69A24C88929cf20F7"), + RegolithTime: u64Ptr(0), + CanyonTime: u64Ptr(0), + DeltaTime: u64Ptr(0), + EcotoneTime: u64Ptr(1706634000), + FjordTime: u64Ptr(1715961600), + GraniteTime: u64Ptr(1723046400), + HoloceneTime: u64Ptr(1731682800), ProtocolVersionsAddress: common.HexToAddress("0x252CbE9517F731C618961D890D534183822dcC8d"), } diff --git a/op-node/node/api.go b/op-node/node/api.go index ccd4a3b81bb3..66fea4895ede 100644 --- a/op-node/node/api.go +++ b/op-node/node/api.go @@ -135,6 +135,8 @@ func (n *nodeAPI) OutputAtBlock(ctx context.Context, number hexutil.Uint64) (*et return nil, fmt.Errorf("failed to get L2 block ref with sync status: %w", err) } + // OutputV0AtBlock uses the WithdrawalsRoot in the block header as the value for the + // output MessagePasserStorageRoot, if Isthmus hard fork has activated. output, err := n.client.OutputV0AtBlock(ctx, ref.Hash) if err != nil { return nil, fmt.Errorf("failed to get L2 output at block %s: %w", ref, err) diff --git a/op-node/p2p/gossip.go b/op-node/p2p/gossip.go index f835dfb2f163..7462596b76ea 100644 --- a/op-node/p2p/gossip.go +++ b/op-node/p2p/gossip.go @@ -78,10 +78,19 @@ func blocksTopicV3(cfg *rollup.Config) string { return fmt.Sprintf("/optimism/%s/2/blocks", cfg.L2ChainID.String()) } +func blocksTopicV4(cfg *rollup.Config) string { + return fmt.Sprintf("/optimism/%s/3/blocks", cfg.L2ChainID.String()) +} + // BuildSubscriptionFilter builds a simple subscription filter, // to help protect against peers spamming useless subscriptions. func BuildSubscriptionFilter(cfg *rollup.Config) pubsub.SubscriptionFilter { - return pubsub.NewAllowlistSubscriptionFilter(blocksTopicV1(cfg), blocksTopicV2(cfg), blocksTopicV3(cfg)) // add more topics here in the future, if any. + return pubsub.NewAllowlistSubscriptionFilter( + blocksTopicV1(cfg), + blocksTopicV2(cfg), + blocksTopicV3(cfg), + blocksTopicV4(cfg), // add more topics here in the future, if any. + ) } var msgBufPool = sync.Pool{New: func() any { @@ -386,6 +395,10 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti return pubsub.ValidationReject } + if blockVersion.HasWithdrawalsRoot() && payload.WithdrawalsRoot == nil { + log.Warn("payload is on v4 topic, but has nil withdrawals root", "bad_hash", payload.BlockHash.String()) + } + seen, ok := blockHeightLRU.Get(uint64(payload.BlockNumber)) if !ok { seen = new(seenBlocks) @@ -450,6 +463,7 @@ type GossipTopicInfo interface { BlocksTopicV1Peers() []peer.ID BlocksTopicV2Peers() []peer.ID BlocksTopicV3Peers() []peer.ID + BlocksTopicV4Peers() []peer.ID } type GossipOut interface { @@ -485,6 +499,7 @@ type publisher struct { blocksV1 *blockTopic blocksV2 *blockTopic blocksV3 *blockTopic + blocksV4 *blockTopic runCfg GossipRuntimeConfig } @@ -507,7 +522,12 @@ func combinePeers(allPeers ...[]peer.ID) []peer.ID { } func (p *publisher) AllBlockTopicsPeers() []peer.ID { - return combinePeers(p.BlocksTopicV1Peers(), p.BlocksTopicV2Peers(), p.BlocksTopicV3Peers()) + return combinePeers( + p.BlocksTopicV1Peers(), + p.BlocksTopicV2Peers(), + p.BlocksTopicV3Peers(), + p.BlocksTopicV4Peers(), + ) } func (p *publisher) BlocksTopicV1Peers() []peer.ID { @@ -522,6 +542,10 @@ func (p *publisher) BlocksTopicV3Peers() []peer.ID { return p.blocksV3.topic.ListPeers() } +func (p *publisher) BlocksTopicV4Peers() []peer.ID { + return p.blocksV4.topic.ListPeers() +} + func (p *publisher) PublishL2Payload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope, signer Signer) error { res := msgBufPool.Get().(*[]byte) buf := bytes.NewBuffer((*res)[:0]) @@ -554,7 +578,9 @@ func (p *publisher) PublishL2Payload(ctx context.Context, envelope *eth.Executio // This also copies the data, freeing up the original buffer to go back into the pool out := snappy.Encode(nil, data) - if p.cfg.IsEcotone(uint64(envelope.ExecutionPayload.Timestamp)) { + if p.cfg.IsIsthmus(uint64(envelope.ExecutionPayload.Timestamp)) { + return p.blocksV4.topic.Publish(ctx, out) + } else if p.cfg.IsEcotone(uint64(envelope.ExecutionPayload.Timestamp)) { return p.blocksV3.topic.Publish(ctx, out) } else if p.cfg.IsCanyon(uint64(envelope.ExecutionPayload.Timestamp)) { return p.blocksV2.topic.Publish(ctx, out) @@ -597,6 +623,14 @@ func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Con return nil, fmt.Errorf("failed to setup blocks v3 p2p: %w", err) } + v4Logger := log.New("topic", "blocksV4") + blocksV4Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv4", v4Logger, BuildBlocksValidator(v4Logger, cfg, runCfg, eth.BlockV4))) + blocksV4, err := newBlockTopic(p2pCtx, blocksTopicV4(cfg), ps, v4Logger, gossipIn, blocksV4Validator) + if err != nil { + p2pCancel() + return nil, fmt.Errorf("failed to setup blocks v4 p2p: %w", err) + } + return &publisher{ log: log, cfg: cfg, @@ -604,6 +638,7 @@ func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Con blocksV1: blocksV1, blocksV2: blocksV2, blocksV3: blocksV3, + blocksV4: blocksV4, runCfg: runCfg, }, nil } diff --git a/op-node/p2p/rpc_server.go b/op-node/p2p/rpc_server.go index 1aa587fcc89f..e1c70a177c90 100644 --- a/op-node/p2p/rpc_server.go +++ b/op-node/p2p/rpc_server.go @@ -206,6 +206,7 @@ type PeerStats struct { BlocksTopic uint `json:"blocksTopic"` BlocksTopicV2 uint `json:"blocksTopicV2"` BlocksTopicV3 uint `json:"blocksTopicV3"` + BlocksTopicV4 uint `json:"blocksTopicV4"` Banned uint `json:"banned"` Known uint `json:"known"` } @@ -223,6 +224,7 @@ func (s *APIBackend) PeerStats(_ context.Context) (*PeerStats, error) { BlocksTopic: uint(len(s.node.GossipOut().BlocksTopicV1Peers())), BlocksTopicV2: uint(len(s.node.GossipOut().BlocksTopicV2Peers())), BlocksTopicV3: uint(len(s.node.GossipOut().BlocksTopicV3Peers())), + BlocksTopicV4: uint(len(s.node.GossipOut().BlocksTopicV4Peers())), Banned: 0, Known: uint(len(pstore.Peers())), } diff --git a/op-node/rollup/attributes/attributes_test.go b/op-node/rollup/attributes/attributes_test.go index 05fbd21fe784..69880885a8e1 100644 --- a/op-node/rollup/attributes/attributes_test.go +++ b/op-node/rollup/attributes/attributes_test.go @@ -84,6 +84,8 @@ func TestAttributesHandler(t *testing.T) { EcotoneTime: new(uint64), } + emptyWithdrawals := make(types.Withdrawals, 0) + a1L1Info, err := derive.L1InfoDepositBytes(cfg, cfg.Genesis.SystemConfig, 1, aL1Info, refA0.Time+cfg.BlockTime) require.NoError(t, err) parentBeaconBlockRoot := testutils.RandomHash(rng) @@ -102,6 +104,7 @@ func TestAttributesHandler(t *testing.T) { BaseFeePerGas: eth.Uint256Quantity(*uint256.NewInt(7)), BlockHash: common.Hash{}, Transactions: []eth.Data{a1L1Info}, + Withdrawals: &emptyWithdrawals, }, ParentBeaconBlockRoot: &parentBeaconBlockRoot} // fix up the block-hash payloadA1.ExecutionPayload.BlockHash, _ = payloadA1.CheckBlockHash() @@ -139,6 +142,7 @@ func TestAttributesHandler(t *testing.T) { BaseFeePerGas: eth.Uint256Quantity(*uint256.NewInt(7)), BlockHash: common.Hash{}, Transactions: []eth.Data{a1L1Info}, + Withdrawals: &emptyWithdrawals, }, ParentBeaconBlockRoot: &parentBeaconBlockRoot} // fix up the block-hash payloadA1Alt.ExecutionPayload.BlockHash, _ = payloadA1Alt.CheckBlockHash() diff --git a/op-node/rollup/attributes/engine_consolidate.go b/op-node/rollup/attributes/engine_consolidate.go index 3cd9e8931210..3b99c92df567 100644 --- a/op-node/rollup/attributes/engine_consolidate.go +++ b/op-node/rollup/attributes/engine_consolidate.go @@ -55,7 +55,7 @@ func AttributesMatchBlock(rollupCfg *rollup.Config, attrs *eth.PayloadAttributes if *attrs.GasLimit != block.GasLimit { return fmt.Errorf("gas limit does not match. expected %d. got: %d", *attrs.GasLimit, block.GasLimit) } - if withdrawalErr := checkWithdrawalsMatch(attrs.Withdrawals, block.Withdrawals); withdrawalErr != nil { + if withdrawalErr := checkWithdrawals(rollupCfg, attrs, block); withdrawalErr != nil { return withdrawalErr } if err := checkParentBeaconBlockRootMatch(attrs.ParentBeaconBlockRoot, envelope.ParentBeaconBlockRoot); err != nil { @@ -82,31 +82,44 @@ func checkParentBeaconBlockRootMatch(attrRoot, blockRoot *common.Hash) error { return nil } -func checkWithdrawalsMatch(attrWithdrawals *types.Withdrawals, blockWithdrawals *types.Withdrawals) error { - if attrWithdrawals == nil && blockWithdrawals == nil { - 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 { + if attrs == nil || block == nil { + return fmt.Errorf("nil attributes or block") } - if attrWithdrawals == nil && blockWithdrawals != nil { - return fmt.Errorf("expected withdrawals in block to be nil, actual %v", *blockWithdrawals) - } - - if attrWithdrawals != nil && blockWithdrawals == nil { - return fmt.Errorf("expected withdrawals in block to be non-nil %v, actual nil", *attrWithdrawals) - } + attrWithdrawals := attrs.Withdrawals + blockWithdrawals := block.Withdrawals - if len(*attrWithdrawals) != len(*blockWithdrawals) { - return fmt.Errorf("expected withdrawals in block to be %d, actual %d", len(*attrWithdrawals), len(*blockWithdrawals)) - } - - for idx, expected := range *attrWithdrawals { - actual := (*blockWithdrawals)[idx] - - if *expected != *actual { - return fmt.Errorf("expected withdrawal %d to be %v, actual %v", idx, expected, actual) + // If we are pre-canyon, attributes should have nil withdrawals list and withdrawalsRoot + if !rollupCfg.IsCanyon(uint64(block.Timestamp)) { + if attrWithdrawals != nil { + return fmt.Errorf("pre-canyon: expected withdrawals in attributes to be nil, actual %v", *attrWithdrawals) + } + } else if rollupCfg.IsIsthmus(uint64(block.Timestamp)) { + // isthmus is active, we should have an empty withdrawals list and non-nil withdrawalsRoot + if !(blockWithdrawals != nil && len(*blockWithdrawals) == 0) { + return fmt.Errorf("isthmus: expected withdrawals in block to be non-nil and empty, actual %v", *blockWithdrawals) + } + if block.WithdrawalsRoot == nil { + return fmt.Errorf("isthmus: expected withdrawalsRoot in block to be non-nil") + } + if !(attrWithdrawals != nil && len(*attrWithdrawals) == 0) { + return fmt.Errorf("isthmus: expected withdrawals in attributes to be non-nil and empty, actual %v", *attrWithdrawals) + } + } else { + // pre-isthmus, post-canyon + if !(blockWithdrawals != nil && len(*blockWithdrawals) == 0) { + return fmt.Errorf("pre-isthmus: expected withdrawals in block to be non-nil and empty, actual %v", *blockWithdrawals) + } + if block.WithdrawalsRoot != nil { + return fmt.Errorf("pre-isthmus: expected withdrawalsRoot in block to be nil, actual %v", *block.WithdrawalsRoot) + } + if !(attrWithdrawals != nil && len(*attrWithdrawals) == 0) { + return fmt.Errorf("pre-isthmus: expected withdrawals in attributes to be non-nil and empty, actual %v", *attrWithdrawals) } } - return nil } diff --git a/op-node/rollup/attributes/engine_consolidate_test.go b/op-node/rollup/attributes/engine_consolidate_test.go index 015e34f5d3d4..da271e58e023 100644 --- a/op-node/rollup/attributes/engine_consolidate_test.go +++ b/op-node/rollup/attributes/engine_consolidate_test.go @@ -24,7 +24,6 @@ var ( validParentBeaconRoot = common.HexToHash("0x456") validPrevRandao = eth.Bytes32(common.HexToHash("0x789")) validGasLimit = eth.Uint64Quantity(1000) - validWithdrawals = types.Withdrawals{} validFeeRecipient = predeploys.SequencerFeeVaultAddr ) @@ -43,7 +42,7 @@ func ecotoneArgs() args { Timestamp: validTimestamp, PrevRandao: validPrevRandao, GasLimit: validGasLimit, - Withdrawals: &validWithdrawals, + Withdrawals: nil, FeeRecipient: validFeeRecipient, }, }, @@ -52,7 +51,7 @@ func ecotoneArgs() args { PrevRandao: validPrevRandao, GasLimit: &validGasLimit, ParentBeaconBlockRoot: &validParentBeaconRoot, - Withdrawals: &validWithdrawals, + Withdrawals: nil, SuggestedFeeRecipient: validFeeRecipient, }, parentHash: validParentHash, @@ -145,137 +144,283 @@ func createMismatchedFeeRecipient() args { } func TestAttributesMatch(t *testing.T) { + canyonTimeInFuture := uint64(100) + canyonTimeInPast := uint64(0) + isthmusTimeInFuture := uint64(250) + + rollupCfgPreCanyonChecks := &rollup.Config{CanyonTime: &canyonTimeInFuture} + rollupCfgPreIsthmusChecks := &rollup.Config{CanyonTime: &canyonTimeInPast, IsthmusTime: &isthmusTimeInFuture} + rollupCfg := &rollup.Config{} tests := []struct { shouldMatch bool args args + rollupCfg *rollup.Config + desc string }{ { shouldMatch: true, args: ecotoneArgs(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "ecotoneArgs", }, { shouldMatch: true, args: canyonArgs(), + rollupCfg: rollupCfgPreIsthmusChecks, + desc: "canyonArgs", }, { shouldMatch: true, args: bedrockArgs(), + rollupCfg: rollupCfgPreIsthmusChecks, + desc: "bedrockArgs", }, { shouldMatch: false, args: mismatchedParentHashArgs(), + rollupCfg: rollupCfgPreIsthmusChecks, + desc: "mismatchedParentHashArgs", }, { shouldMatch: false, args: ecotoneNoParentBeaconBlockRoot(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "ecotoneNoParentBeaconBlockRoot", }, { shouldMatch: false, args: ecotoneUnexpectedParentBeaconBlockRoot(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "ecotoneUnexpectedParentBeaconBlockRoot", }, { shouldMatch: false, args: ecotoneMismatchParentBeaconBlockRoot(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "ecotoneMismatchParentBeaconBlockRoot", }, { shouldMatch: true, args: ecotoneMismatchParentBeaconBlockRootPtr(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "ecotoneMismatchParentBeaconBlockRootPtr", }, { shouldMatch: true, args: ecotoneNilParentBeaconBlockRoots(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "ecotoneNilParentBeaconBlockRoots", }, { shouldMatch: false, args: createMismatchedPrevRandao(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "createMismatchedPrevRandao", }, { shouldMatch: false, args: createMismatchedGasLimit(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "createMismatchedGasLimit", }, { shouldMatch: false, args: createNilGasLimit(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "createNilGasLimit", }, { shouldMatch: false, args: createMismatchedTimestamp(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "createMismatchedTimestamp", }, { shouldMatch: false, args: createMismatchedFeeRecipient(), + rollupCfg: rollupCfgPreCanyonChecks, + desc: "createMismatchedFeeRecipient", }, } - for i, test := range tests { + 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 %d", i) + require.NoError(t, err, "fail: %s", test.desc) } else { - require.Error(t, err, "fail %d", i) + require.Error(t, err, "fail: %s", test.desc) } } } func TestWithdrawalsMatch(t *testing.T) { + canyonTimeInFuture := uint64(100) + canyonTimeInPast := uint64(0) + isthmusTimeInPast := uint64(150) + isthmusTimeInFuture := uint64(250) + + emptyWithdrawals := make(types.Withdrawals, 0) + + rollupCfgPreCanyonChecks := &rollup.Config{CanyonTime: &canyonTimeInFuture} + rollupCfgPreIsthmusChecks := &rollup.Config{CanyonTime: &canyonTimeInPast, IsthmusTime: &isthmusTimeInFuture} + rollupCfgPostIsthmusChecks := &rollup.Config{CanyonTime: &canyonTimeInPast, IsthmusTime: &isthmusTimeInPast} + tests := []struct { - attrs *types.Withdrawals - block *types.Withdrawals - shouldMatch bool + cfg *rollup.Config + attrs *eth.PayloadAttributes + block *eth.ExecutionPayload + checkFails bool + desc string }{ { - attrs: nil, - block: nil, - shouldMatch: true, + cfg: rollupCfgPreCanyonChecks, + attrs: nil, + block: nil, + checkFails: true, + desc: "nil attributes/block", }, { - attrs: &types.Withdrawals{}, - block: nil, - shouldMatch: false, + cfg: rollupCfgPreCanyonChecks, + attrs: ð.PayloadAttributes{Withdrawals: nil}, + block: ð.ExecutionPayload{Timestamp: 0}, + checkFails: false, + desc: "pre-canyon: nil attr withdrawals", }, { - attrs: nil, - block: &types.Withdrawals{}, - shouldMatch: false, + cfg: rollupCfgPreCanyonChecks, + attrs: ð.PayloadAttributes{ + Withdrawals: &types.Withdrawals{ + &types.Withdrawal{ + Index: 1, + }, + }, + }, + block: ð.ExecutionPayload{Timestamp: 0}, + checkFails: true, + desc: "pre-canyon: non-nil withdrawals", }, { - attrs: &types.Withdrawals{}, - block: &types.Withdrawals{}, - shouldMatch: true, + cfg: rollupCfgPostIsthmusChecks, + attrs: ð.PayloadAttributes{}, + block: ð.ExecutionPayload{ + Timestamp: 200, + Withdrawals: &types.Withdrawals{ + &types.Withdrawal{ + Index: 1, + }, + }, + }, + checkFails: true, + desc: "post-isthmus: non-empty block withdrawals list", }, { - attrs: &types.Withdrawals{ - { - Index: 1, + cfg: rollupCfgPostIsthmusChecks, + attrs: ð.PayloadAttributes{}, + block: ð.ExecutionPayload{ + Timestamp: 200, + WithdrawalsRoot: nil, + Withdrawals: &emptyWithdrawals, + }, + checkFails: true, + desc: "post-isthmus: nil block withdrawalsRoot", + }, + { + cfg: rollupCfgPostIsthmusChecks, + attrs: ð.PayloadAttributes{ + Withdrawals: &types.Withdrawals{ + &types.Withdrawal{ + Index: 1, + }, }, }, - block: &types.Withdrawals{}, - shouldMatch: false, + block: ð.ExecutionPayload{ + Timestamp: 200, + WithdrawalsRoot: &common.Hash{}, + Withdrawals: &emptyWithdrawals, + }, + checkFails: true, + desc: "post-isthmus: non-empty attr withdrawals list", + }, + { + cfg: rollupCfgPostIsthmusChecks, + attrs: ð.PayloadAttributes{ + Withdrawals: &emptyWithdrawals, + }, + block: ð.ExecutionPayload{ + Timestamp: 200, + WithdrawalsRoot: &common.Hash{}, + Withdrawals: &emptyWithdrawals, + }, + checkFails: false, + desc: "post-isthmus: non-empty block withdrawalsRoot and empty block/attr withdrawals list", }, { - attrs: &types.Withdrawals{ - { - Index: 1, + cfg: rollupCfgPreIsthmusChecks, + attrs: ð.PayloadAttributes{}, + block: ð.ExecutionPayload{ + Timestamp: 200, + Withdrawals: &types.Withdrawals{ + &types.Withdrawal{ + Index: 1, + }, }, }, - block: &types.Withdrawals{ - { - Index: 2, + checkFails: true, + desc: "pre-isthmus: non-empty block withdrawals list", + }, + { + cfg: rollupCfgPreIsthmusChecks, + attrs: ð.PayloadAttributes{}, + block: ð.ExecutionPayload{ + Timestamp: 200, + Withdrawals: &types.Withdrawals{}, + WithdrawalsRoot: &common.Hash{}, + }, + checkFails: true, + desc: "pre-isthmus: non-empty block withdrawalsRoot", + }, + { + cfg: rollupCfgPreIsthmusChecks, + attrs: ð.PayloadAttributes{ + Withdrawals: &types.Withdrawals{ + &types.Withdrawal{ + Index: 1, + }, }, }, - shouldMatch: false, + block: ð.ExecutionPayload{ + Timestamp: 200, + Withdrawals: &types.Withdrawals{}, + WithdrawalsRoot: nil, + }, + checkFails: true, + desc: "pre-isthmus: non-empty attr withdrawals list", + }, + { + cfg: rollupCfgPreIsthmusChecks, + attrs: ð.PayloadAttributes{ + Withdrawals: &emptyWithdrawals, + }, + block: ð.ExecutionPayload{ + Timestamp: 200, + WithdrawalsRoot: nil, + Withdrawals: &emptyWithdrawals, + }, + checkFails: false, + desc: "pre-isthmus: nil block withdrawalsRoot and empty block/attr withdrawals list", }, } for _, test := range tests { - err := checkWithdrawalsMatch(test.attrs, test.block) + t.Log(test.desc) + err := checkWithdrawals(test.cfg, test.attrs, test.block) - if test.shouldMatch { - require.NoError(t, err) + if test.checkFails { + require.Error(t, err, "test: %s", test.desc) } else { - require.Error(t, err) + require.NoError(t, err, "test: %s", test.desc) } } } diff --git a/op-node/rollup/chain_spec.go b/op-node/rollup/chain_spec.go index 1ddcb3190290..936bb2b17dfa 100644 --- a/op-node/rollup/chain_spec.go +++ b/op-node/rollup/chain_spec.go @@ -41,6 +41,7 @@ const ( Fjord ForkName = "fjord" Granite ForkName = "granite" Holocene ForkName = "holocene" + Isthmus ForkName = "isthmus" Interop ForkName = "interop" // ADD NEW FORKS TO AllForks BELOW! None ForkName = "none" @@ -55,6 +56,7 @@ var AllForks = []ForkName{ Fjord, Granite, Holocene, + Isthmus, Interop, // ADD NEW FORKS HERE! } @@ -114,6 +116,11 @@ func (s *ChainSpec) IsHolocene(t uint64) bool { return s.config.IsHolocene(t) } +// IsIsthmus returns true if t >= isthmus_time +func (s *ChainSpec) IsIsthmus(t uint64) bool { + return s.config.IsIsthmus(t) +} + // MaxChannelBankSize returns the maximum number of bytes the can allocated inside the channel bank // before pruning occurs at the given timestamp. func (s *ChainSpec) MaxChannelBankSize(t uint64) uint64 { @@ -185,6 +192,9 @@ func (s *ChainSpec) CheckForkActivation(log log.Logger, block eth.L2BlockRef) { if s.config.IsHolocene(block.Time) { s.currentFork = Holocene } + if s.config.IsIsthmus(block.Time) { + s.currentFork = Isthmus + } if s.config.IsInterop(block.Time) { s.currentFork = Interop } @@ -209,6 +219,8 @@ func (s *ChainSpec) CheckForkActivation(log log.Logger, block eth.L2BlockRef) { foundActivationBlock = s.config.IsGraniteActivationBlock(block.Time) case Holocene: foundActivationBlock = s.config.IsHoloceneActivationBlock(block.Time) + case Isthmus: + foundActivationBlock = s.config.IsIsthmusActivationBlock(block.Time) case Interop: foundActivationBlock = s.config.IsInteropActivationBlock(block.Time) } diff --git a/op-node/rollup/derive/test/random.go b/op-node/rollup/derive/test/random.go index ba9600da8c82..29b40f50ce9f 100644 --- a/op-node/rollup/derive/test/random.go +++ b/op-node/rollup/derive/test/random.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/testutils" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" ) @@ -17,7 +18,7 @@ import ( // L1 Info Deposit transaction. func RandomL2Block(rng *rand.Rand, txCount int, t time.Time) (*types.Block, []*types.Receipt) { body := types.Body{} - l1Block := types.NewBlock(testutils.RandomHeader(rng), &body, nil, trie.NewStackTrie(nil)) + l1Block := types.NewBlock(testutils.RandomHeader(rng), &body, nil, trie.NewStackTrie(nil), ¶ms.ChainConfig{}) rollupCfg := rollup.Config{} if testutils.RandomBool(rng) { t := uint64(0) diff --git a/op-node/rollup/superchain.go b/op-node/rollup/superchain.go index e4ca043011a2..5d83b0dc301a 100644 --- a/op-node/rollup/superchain.go +++ b/op-node/rollup/superchain.go @@ -92,6 +92,7 @@ func LoadOPStackRollupConfig(chainID uint64) (*Config, error) { FjordTime: chConfig.FjordTime, GraniteTime: chConfig.GraniteTime, HoloceneTime: chConfig.HoloceneTime, + IsthmusTime: chConfig.IsthmusTime, BatchInboxAddress: common.Address(chConfig.BatchInboxAddr), DepositContractAddress: common.Address(addrs.OptimismPortalProxy), L1SystemConfigAddress: common.Address(addrs.SystemConfigProxy), diff --git a/op-node/rollup/types.go b/op-node/rollup/types.go index e5e541a6f7a5..956911961226 100644 --- a/op-node/rollup/types.go +++ b/op-node/rollup/types.go @@ -120,6 +120,10 @@ type Config struct { // Active if HoloceneTime != nil && L2 block timestamp >= *HoloceneTime, inactive otherwise. HoloceneTime *uint64 `json:"holocene_time,omitempty"` + // IsthmusTime sets the activation time of the Isthmus network upgrade. + // Active if IsthmusTime != nil && L2 block timestamp >= *IsthmusTime, inactive otherwise. + IsthmusTime *uint64 `json:"isthmus_time,omitempty"` + // InteropTime sets the activation time for an experimental feature-set, activated like a hardfork. // Active if InteropTime != nil && L2 block timestamp >= *InteropTime, inactive otherwise. InteropTime *uint64 `json:"interop_time,omitempty"` @@ -328,6 +332,9 @@ func (cfg *Config) Check() error { if err := checkFork(cfg.GraniteTime, cfg.HoloceneTime, Granite, Holocene); err != nil { return err } + if err := checkFork(cfg.HoloceneTime, cfg.IsthmusTime, Holocene, Isthmus); err != nil { + return err + } return nil } @@ -404,6 +411,11 @@ func (c *Config) IsHolocene(timestamp uint64) bool { return c.HoloceneTime != nil && timestamp >= *c.HoloceneTime } +// IsIsthmus returns true if the Isthmus hardfork is active at or past the given timestamp. +func (c *Config) IsIsthmus(timestamp uint64) bool { + return c.IsthmusTime != nil && timestamp >= *c.IsthmusTime +} + // IsInterop returns true if the Interop hardfork is active at or past the given timestamp. func (c *Config) IsInterop(timestamp uint64) bool { return c.InteropTime != nil && timestamp >= *c.InteropTime @@ -459,6 +471,14 @@ func (c *Config) IsHoloceneActivationBlock(l2BlockTime uint64) bool { !c.IsHolocene(l2BlockTime-c.BlockTime) } +// IsIsthmusActivationBlock returns whether the specified block is the first block subject to the +// Isthmus upgrade. +func (c *Config) IsIsthmusActivationBlock(l2BlockTime uint64) bool { + return c.IsIsthmus(l2BlockTime) && + l2BlockTime >= c.BlockTime && + !c.IsIsthmus(l2BlockTime-c.BlockTime) +} + func (c *Config) IsInteropActivationBlock(l2BlockTime uint64) bool { return c.IsInterop(l2BlockTime) && l2BlockTime >= c.BlockTime && @@ -482,6 +502,9 @@ func (c *Config) ActivateAtGenesis(hardfork ForkName) { case Interop: c.InteropTime = new(uint64) fallthrough + case Isthmus: + c.IsthmusTime = new(uint64) + fallthrough case Holocene: c.HoloceneTime = new(uint64) fallthrough @@ -621,6 +644,7 @@ func (c *Config) Description(l2Chains map[string]string) string { banner += fmt.Sprintf(" - Fjord: %s\n", fmtForkTimeOrUnset(c.FjordTime)) banner += fmt.Sprintf(" - Granite: %s\n", fmtForkTimeOrUnset(c.GraniteTime)) banner += fmt.Sprintf(" - Holocene: %s\n", fmtForkTimeOrUnset(c.HoloceneTime)) + banner += fmt.Sprintf(" - Isthmus: %s\n", fmtForkTimeOrUnset(c.IsthmusTime)) banner += fmt.Sprintf(" - Interop: %s\n", fmtForkTimeOrUnset(c.InteropTime)) // Report the protocol version banner += fmt.Sprintf("Node supports up to OP-Stack Protocol Version: %s\n", OPStackSupport) @@ -657,6 +681,7 @@ func (c *Config) LogDescription(log log.Logger, l2Chains map[string]string) { "fjord_time", fmtForkTimeOrUnset(c.FjordTime), "granite_time", fmtForkTimeOrUnset(c.GraniteTime), "holocene_time", fmtForkTimeOrUnset(c.HoloceneTime), + "isthmus_time", fmtForkTimeOrUnset(c.IsthmusTime), "interop_time", fmtForkTimeOrUnset(c.InteropTime), "alt_da", c.AltDAConfig != nil, ) diff --git a/op-node/rollup/types_test.go b/op-node/rollup/types_test.go index 11c4db505c96..066ad0f7ec52 100644 --- a/op-node/rollup/types_test.go +++ b/op-node/rollup/types_test.go @@ -250,6 +250,15 @@ func TestActivations(t *testing.T) { return c.IsHolocene(t) }, }, + { + name: "Isthmus", + setUpgradeTime: func(t *uint64, c *Config) { + c.IsthmusTime = t + }, + checkEnabled: func(t uint64, c *Config) bool { + return c.IsIsthmus(t) + }, + }, { name: "Interop", setUpgradeTime: func(t *uint64, c *Config) { @@ -518,10 +527,20 @@ func TestConfig_Check(t *testing.T) { canyonTime := uint64(2) deltaTime := uint64(3) ecotoneTime := uint64(4) + fjordTime := uint64(5) + graniteTime := uint64(6) + holoceneTime := uint64(7) + isthmusTime := uint64(8) + interopTime := uint64(9) cfg.RegolithTime = ®olithTime cfg.CanyonTime = &canyonTime cfg.DeltaTime = &deltaTime cfg.EcotoneTime = &ecotoneTime + cfg.FjordTime = &fjordTime + cfg.GraniteTime = &graniteTime + cfg.HoloceneTime = &holoceneTime + cfg.IsthmusTime = &isthmusTime + cfg.InteropTime = &interopTime }, expectedErr: nil, }, diff --git a/op-program/client/l2/engine_test.go b/op-program/client/l2/engine_test.go index 4b84b2db3cd6..a53e1998e181 100644 --- a/op-program/client/l2/engine_test.go +++ b/op-program/client/l2/engine_test.go @@ -187,7 +187,7 @@ func createL2Block(t *testing.T, number int) *types.Block { body := &types.Body{ Transactions: []*types.Transaction{types.NewTx(tx)}, } - return types.NewBlock(header, body, nil, trie.NewStackTrie(nil)) + return types.NewBlock(header, body, nil, trie.NewStackTrie(nil), ¶ms.ChainConfig{}) } type stubEngineBackend struct { diff --git a/op-program/client/l2/engineapi/block_processor.go b/op-program/client/l2/engineapi/block_processor.go index b2a584088b21..2126271c2d4f 100644 --- a/op-program/client/l2/engineapi/block_processor.go +++ b/op-program/client/l2/engineapi/block_processor.go @@ -6,6 +6,7 @@ import ( "math/big" "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/predeploys" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" @@ -107,6 +108,12 @@ func NewBlockProcessorFromHeader(provider BlockDataProvider, h *types.Header) (* vmenv := mkEVM() core.ProcessParentBlockHash(header.ParentHash, vmenv, statedb) } + if provider.Config().IsIsthmus(header.Time) { + // set the header withdrawals root for Isthmus blocks + mpHash := statedb.GetStorageRoot(predeploys.L2ToL1MessagePasserAddr) + header.WithdrawalsHash = &mpHash + } + return &BlockProcessor{ header: header, state: statedb, diff --git a/op-program/client/l2/engineapi/l2_engine_api.go b/op-program/client/l2/engineapi/l2_engine_api.go index 273483893b37..e7e7580b857b 100644 --- a/op-program/client/l2/engineapi/l2_engine_api.go +++ b/op-program/client/l2/engineapi/l2_engine_api.go @@ -177,6 +177,7 @@ func (ea *L2EngineAPI) startBlock(parent common.Hash, attrs *eth.PayloadAttribut return fmt.Errorf("failed to apply deposit transaction to L2 block (tx %d): %w", i, err) } } + return nil } @@ -301,7 +302,7 @@ func (ea *L2EngineAPI) NewPayloadV1(ctx context.Context, payload *eth.ExecutionP return ð.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.InvalidParams.With(errors.New("withdrawals not supported in V1")) } - return ea.newPayload(ctx, payload, nil, nil) + return ea.newPayload(ctx, payload, nil, nil, ea.backend.Config()) } func (ea *L2EngineAPI) NewPayloadV2(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) { @@ -313,7 +314,7 @@ func (ea *L2EngineAPI) NewPayloadV2(ctx context.Context, payload *eth.ExecutionP return ð.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.InvalidParams.With(errors.New("non-nil withdrawals pre-shanghai")) } - return ea.newPayload(ctx, payload, nil, nil) + return ea.newPayload(ctx, payload, nil, nil, ea.backend.Config()) } // Ported from: https://github.com/ethereum-optimism/op-geth/blob/c50337a60a1309a0f1dca3bf33ed1bb38c46cdd7/eth/catalyst/api.go#L486C1-L507 @@ -338,11 +339,18 @@ func (ea *L2EngineAPI) NewPayloadV3(ctx context.Context, params *eth.ExecutionPa // Payload must have eip-1559 params in ExtraData after Holocene if ea.config().IsHolocene(uint64(params.Timestamp)) { if err := eip1559.ValidateHoloceneExtraData(params.ExtraData); err != nil { - return ð.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.UnsupportedFork.With(errors.New("invalid holocene extraData post-holoocene")) + return ð.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.UnsupportedFork.With(errors.New("invalid holocene extraData post-holocene")) + } + } + + // Payload must have WithdrawalsRoot after Isthmus + if ea.config().IsIsthmus(uint64(params.Timestamp)) { + if params.WithdrawalsRoot == nil { + return ð.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.UnsupportedFork.With(errors.New("nil withdrawalsRoot post-isthmus")) } } - return ea.newPayload(ctx, params, versionedHashes, beaconRoot) + return ea.newPayload(ctx, params, versionedHashes, beaconRoot, ea.backend.Config()) } func (ea *L2EngineAPI) getPayload(_ context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayloadEnvelope, error) { @@ -479,31 +487,32 @@ func toGethWithdrawals(payload *eth.ExecutionPayload) []*types.Withdrawal { return result } -func (ea *L2EngineAPI) newPayload(_ context.Context, payload *eth.ExecutionPayload, hashes []common.Hash, root *common.Hash) (*eth.PayloadStatusV1, error) { +func (ea *L2EngineAPI) newPayload(_ context.Context, payload *eth.ExecutionPayload, hashes []common.Hash, root *common.Hash, config *params.ChainConfig) (*eth.PayloadStatusV1, error) { ea.log.Trace("L2Engine API request received", "method", "ExecutePayload", "number", payload.BlockNumber, "hash", payload.BlockHash) txs := make([][]byte, len(payload.Transactions)) for i, tx := range payload.Transactions { txs[i] = tx } block, err := engine.ExecutableDataToBlock(engine.ExecutableData{ - ParentHash: payload.ParentHash, - FeeRecipient: payload.FeeRecipient, - StateRoot: common.Hash(payload.StateRoot), - ReceiptsRoot: common.Hash(payload.ReceiptsRoot), - LogsBloom: payload.LogsBloom[:], - Random: common.Hash(payload.PrevRandao), - Number: uint64(payload.BlockNumber), - GasLimit: uint64(payload.GasLimit), - GasUsed: uint64(payload.GasUsed), - Timestamp: uint64(payload.Timestamp), - ExtraData: payload.ExtraData, - BaseFeePerGas: (*uint256.Int)(&payload.BaseFeePerGas).ToBig(), - BlockHash: payload.BlockHash, - Transactions: txs, - Withdrawals: toGethWithdrawals(payload), - ExcessBlobGas: (*uint64)(payload.ExcessBlobGas), - BlobGasUsed: (*uint64)(payload.BlobGasUsed), - }, hashes, root) + ParentHash: payload.ParentHash, + FeeRecipient: payload.FeeRecipient, + StateRoot: common.Hash(payload.StateRoot), + ReceiptsRoot: common.Hash(payload.ReceiptsRoot), + LogsBloom: payload.LogsBloom[:], + Random: common.Hash(payload.PrevRandao), + Number: uint64(payload.BlockNumber), + GasLimit: uint64(payload.GasLimit), + GasUsed: uint64(payload.GasUsed), + Timestamp: uint64(payload.Timestamp), + ExtraData: payload.ExtraData, + BaseFeePerGas: (*uint256.Int)(&payload.BaseFeePerGas).ToBig(), + BlockHash: payload.BlockHash, + Transactions: txs, + Withdrawals: toGethWithdrawals(payload), + ExcessBlobGas: (*uint64)(payload.ExcessBlobGas), + BlobGasUsed: (*uint64)(payload.BlobGasUsed), + WithdrawalsRoot: payload.WithdrawalsRoot, + }, hashes, root, config) if err != nil { log.Debug("Invalid NewPayload params", "params", payload, "error", err) return ð.PayloadStatusV1{Status: eth.ExecutionInvalidBlockHash}, nil diff --git a/op-service/eth/block_info.go b/op-service/eth/block_info.go index 268c6d934b6e..b53f6279f4da 100644 --- a/op-service/eth/block_info.go +++ b/op-service/eth/block_info.go @@ -26,6 +26,7 @@ type BlockInfo interface { GasUsed() uint64 GasLimit() uint64 ParentBeaconRoot() *common.Hash // Dencun extension + WithdrawalsRoot() *common.Hash // Isthmus extension // HeaderRLP returns the RLP of the block header as per consensus rules // Returns an error if the header RLP could not be written @@ -72,6 +73,10 @@ func (b blockInfo) ParentBeaconRoot() *common.Hash { return b.Block.BeaconRoot() } +func (b blockInfo) WithdrawalsRoot() *common.Hash { + return b.Header().WithdrawalsHash +} + func BlockToInfo(b *types.Block) BlockInfo { return blockInfo{b} } @@ -133,6 +138,10 @@ func (h headerBlockInfo) ParentBeaconRoot() *common.Hash { return h.Header.ParentBeaconRoot } +func (h headerBlockInfo) WithdrawalsRoot() *common.Hash { + return h.Header.WithdrawalsHash +} + func (h headerBlockInfo) HeaderRLP() ([]byte, error) { return rlp.EncodeToBytes(h.Header) } diff --git a/op-service/eth/ssz.go b/op-service/eth/ssz.go index ae3d89ec3614..c8a165cc33db 100644 --- a/op-service/eth/ssz.go +++ b/op-service/eth/ssz.go @@ -18,6 +18,7 @@ const ( // iota is reset to 0 BlockV1 BlockVersion = iota BlockV2 BlockV3 + BlockV4 ) // ExecutionPayload and ExecutionPayloadEnvelope are the only SSZ types we have to marshal/unmarshal, @@ -49,9 +50,12 @@ const ( // V1 + Withdrawals offset blockV2FixedPart = blockV1FixedPart + 4 - // V2 + BlobGasUed + ExcessBlobGas + // V2 + BlobGasUsed + ExcessBlobGas blockV3FixedPart = blockV2FixedPart + 8 + 8 + // V3 + WithdrawalsRoot + blockV4FixedPart = blockV3FixedPart + 32 + withdrawalSize = 8 + 8 + 20 + 8 // MAX_TRANSACTIONS_PER_PAYLOAD in consensus spec @@ -64,19 +68,25 @@ const ( ) func (v BlockVersion) HasBlobProperties() bool { - return v == BlockV3 + return v == BlockV3 || v == BlockV4 } func (v BlockVersion) HasWithdrawals() bool { - return v == BlockV2 || v == BlockV3 + return v == BlockV2 || v == BlockV3 || v == BlockV4 } func (v BlockVersion) HasParentBeaconBlockRoot() bool { - return v == BlockV3 + return v == BlockV3 || v == BlockV4 +} + +func (v BlockVersion) HasWithdrawalsRoot() bool { + return v == BlockV4 } func executionPayloadFixedPart(version BlockVersion) uint32 { - if version == BlockV3 { + if version == BlockV4 { + return blockV4FixedPart + } else if version == BlockV3 { return blockV3FixedPart } else if version == BlockV2 { return blockV2FixedPart @@ -86,7 +96,9 @@ func executionPayloadFixedPart(version BlockVersion) uint32 { } func (payload *ExecutionPayload) inferVersion() BlockVersion { - if payload.ExcessBlobGas != nil && payload.BlobGasUsed != nil { + if payload.WithdrawalsRoot != nil { + return BlockV4 + } else if payload.ExcessBlobGas != nil && payload.BlobGasUsed != nil { return BlockV3 } else if payload.Withdrawals != nil { return BlockV2 @@ -197,7 +209,8 @@ func (payload *ExecutionPayload) MarshalSSZ(w io.Writer) (n int, err error) { offset += 4 } - if payload.inferVersion() == BlockV3 { + payloadVersion := payload.inferVersion() + if payloadVersion == BlockV3 || payloadVersion == BlockV4 { if payload.BlobGasUsed == nil || payload.ExcessBlobGas == nil { return 0, errors.New("cannot encode ecotone payload without dencun header attributes") } @@ -207,6 +220,14 @@ func (payload *ExecutionPayload) MarshalSSZ(w io.Writer) (n int, err error) { offset += 8 } + if payloadVersion == BlockV4 { + if payload.WithdrawalsRoot == nil { + return 0, errors.New("cannot encode Isthmus payload without withdrawals root") + } + copy(buf[offset:offset+32], (*payload.WithdrawalsRoot)[:]) + offset += 32 + } + if payload.Withdrawals != nil && offset != fixedSize { panic("withdrawals - fixed part size is inconsistent") } @@ -330,7 +351,16 @@ func (payload *ExecutionPayload) UnmarshalSSZ(version BlockVersion, scope uint32 offset += 8 excessBlobGas := binary.LittleEndian.Uint64(buf[offset : offset+8]) payload.ExcessBlobGas = (*Uint64Quantity)(&excessBlobGas) + offset += 8 } + + if version == BlockV4 { + withdrawalsRoot := common.Hash{} + copy(withdrawalsRoot[:], buf[offset:offset+32]) + payload.WithdrawalsRoot = &withdrawalsRoot + offset += 32 + } + _ = offset // for future extensions: we keep the offset accurate for extensions if transactionsOffset > extraDataOffset+32 || transactionsOffset > scope { diff --git a/op-service/eth/ssz_test.go b/op-service/eth/ssz_test.go index c08c5fa32040..b272aea5f4e4 100644 --- a/op-service/eth/ssz_test.go +++ b/op-service/eth/ssz_test.go @@ -249,6 +249,79 @@ func FuzzExecutionPayloadMarshalUnmarshalV3(f *testing.F) { }) } +func FuzzExecutionPayloadMarshalUnmarshalV4(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte, a, b, c, d uint64, extraData []byte, txs uint16, txsData []byte, wCount uint16, blobGasUsed, excessBlobGas uint64) { + if len(data) < 32+20+32+32+256+32+32+32+32 { + return + } + var payload ExecutionPayload + payload.ParentHash = *(*common.Hash)(data[:32]) + data = data[32:] + payload.FeeRecipient = *(*common.Address)(data[:20]) + data = data[20:] + payload.StateRoot = *(*Bytes32)(data[:32]) + data = data[32:] + payload.ReceiptsRoot = *(*Bytes32)(data[:32]) + data = data[32:] + payload.LogsBloom = *(*Bytes256)(data[:256]) + data = data[256:] + payload.PrevRandao = *(*Bytes32)(data[:32]) + data = data[32:] + payload.BlockNumber = Uint64Quantity(a) + payload.GasLimit = Uint64Quantity(a) + payload.GasUsed = Uint64Quantity(a) + payload.Timestamp = Uint64Quantity(a) + payload.BlobGasUsed = (*Uint64Quantity)(&blobGasUsed) + payload.ExcessBlobGas = (*Uint64Quantity)(&excessBlobGas) + payload.WithdrawalsRoot = (*common.Hash)(data[:32]) + if len(extraData) > 32 { + extraData = extraData[:32] + } + payload.ExtraData = extraData + (*uint256.Int)(&payload.BaseFeePerGas).SetBytes(data[:32]) + payload.BlockHash = *(*common.Hash)(data[:32]) + payload.Transactions = make([]Data, txs) + for i := 0; i < int(txs); i++ { + if len(txsData) < 2 { + payload.Transactions[i] = make(Data, 0) + continue + } + txSize := binary.LittleEndian.Uint16(txsData[:2]) + txsData = txsData[2:] + if int(txSize) > len(txsData) { + txSize = uint16(len(txsData)) + } + payload.Transactions[i] = txsData[:txSize] + txsData = txsData[txSize:] + } + + wCount = wCount % maxWithdrawalsPerPayload + withdrawals := make(types.Withdrawals, wCount) + for i := 0; i < int(wCount); i++ { + withdrawals[i] = &types.Withdrawal{ + Index: a, + Validator: b, + Address: common.BytesToAddress(data[:20]), + Amount: c, + } + } + payload.Withdrawals = &withdrawals + + var buf bytes.Buffer + if _, err := payload.MarshalSSZ(&buf); err != nil { + t.Fatalf("failed to marshal ExecutionPayload: %v", err) + } + var roundTripped ExecutionPayload + err := roundTripped.UnmarshalSSZ(BlockV4, uint32(len(buf.Bytes())), bytes.NewReader(buf.Bytes())) + if err != nil { + t.Fatalf("failed to decode previously marshalled payload: %v", err) + } + if diff := cmp.Diff(payload, roundTripped); diff != "" { + t.Fatalf("The data did not round trip correctly:\n%s", diff) + } + }) +} + func FuzzOBP01(f *testing.F) { payload := &ExecutionPayload{ ExtraData: make([]byte, 32), diff --git a/op-service/eth/types.go b/op-service/eth/types.go index 122e9a4df783..00e4b81688f1 100644 --- a/op-service/eth/types.go +++ b/op-service/eth/types.go @@ -231,6 +231,8 @@ type ExecutionPayload struct { BlobGasUsed *Uint64Quantity `json:"blobGasUsed,omitempty"` // Nil if not present (Bedrock, Canyon, Delta) ExcessBlobGas *Uint64Quantity `json:"excessBlobGas,omitempty"` + // Nil if not present (Isthmus) + WithdrawalsRoot *common.Hash `json:"withdrawalsRoot,omitempty"` } func (payload *ExecutionPayload) ID() BlockID { @@ -256,6 +258,10 @@ func (payload *ExecutionPayload) CanyonBlock() bool { return payload.Withdrawals != nil } +func (payload *ExecutionPayload) IsthmusBlock() bool { + return payload.WithdrawalsRoot != nil +} + // CheckBlockHash recomputes the block hash and returns if the embedded block hash matches. func (envelope *ExecutionPayloadEnvelope) CheckBlockHash() (actual common.Hash, ok bool) { payload := envelope.ExecutionPayload @@ -283,7 +289,9 @@ func (envelope *ExecutionPayloadEnvelope) CheckBlockHash() (actual common.Hash, ParentBeaconRoot: envelope.ParentBeaconBlockRoot, } - if payload.CanyonBlock() { + if payload.IsthmusBlock() { + header.WithdrawalsHash = payload.WithdrawalsRoot + } else if payload.CanyonBlock() { withdrawalHash := types.DeriveSha(*payload.Withdrawals, hasher) header.WithdrawalsHash = &withdrawalHash } @@ -307,22 +315,23 @@ func BlockAsPayload(bl *types.Block, shanghaiTime *uint64) (*ExecutionPayload, e } payload := &ExecutionPayload{ - ParentHash: bl.ParentHash(), - FeeRecipient: bl.Coinbase(), - StateRoot: Bytes32(bl.Root()), - ReceiptsRoot: Bytes32(bl.ReceiptHash()), - LogsBloom: Bytes256(bl.Bloom()), - PrevRandao: Bytes32(bl.MixDigest()), - BlockNumber: Uint64Quantity(bl.NumberU64()), - GasLimit: Uint64Quantity(bl.GasLimit()), - GasUsed: Uint64Quantity(bl.GasUsed()), - Timestamp: Uint64Quantity(bl.Time()), - ExtraData: bl.Extra(), - BaseFeePerGas: Uint256Quantity(*baseFee), - BlockHash: bl.Hash(), - Transactions: opaqueTxs, - ExcessBlobGas: (*Uint64Quantity)(bl.ExcessBlobGas()), - BlobGasUsed: (*Uint64Quantity)(bl.BlobGasUsed()), + ParentHash: bl.ParentHash(), + FeeRecipient: bl.Coinbase(), + StateRoot: Bytes32(bl.Root()), + ReceiptsRoot: Bytes32(bl.ReceiptHash()), + LogsBloom: Bytes256(bl.Bloom()), + PrevRandao: Bytes32(bl.MixDigest()), + BlockNumber: Uint64Quantity(bl.NumberU64()), + GasLimit: Uint64Quantity(bl.GasLimit()), + GasUsed: Uint64Quantity(bl.GasUsed()), + Timestamp: Uint64Quantity(bl.Time()), + ExtraData: bl.Extra(), + BaseFeePerGas: Uint256Quantity(*baseFee), + BlockHash: bl.Hash(), + Transactions: opaqueTxs, + ExcessBlobGas: (*Uint64Quantity)(bl.ExcessBlobGas()), + BlobGasUsed: (*Uint64Quantity)(bl.BlobGasUsed()), + WithdrawalsRoot: bl.Header().WithdrawalsHash, } if shanghaiTime != nil && uint64(payload.Timestamp) >= *shanghaiTime { diff --git a/op-service/sources/eth_client.go b/op-service/sources/eth_client.go index 39587fd19750..f745ea852b0f 100644 --- a/op-service/sources/eth_client.go +++ b/op-service/sources/eth_client.go @@ -113,11 +113,18 @@ type EthClient struct { // cache payloads by hash // common.Hash -> *eth.ExecutionPayload payloadsCache *caching.LRUCache[common.Hash, *eth.ExecutionPayloadEnvelope] + + // any checks for if RPC response is valid + respChecker RPCRespCheck +} + +type RPCRespCheck interface { + ValidateWithdrawals(withdrawals *types.Withdrawals, withdrawalRoot *common.Hash) error } // NewEthClient returns an [EthClient], wrapping an RPC with bindings to fetch ethereum data with added error logging, // metric tracking, and caching. The [EthClient] uses a [LimitRPC] wrapper to limit the number of concurrent RPC requests. -func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, config *EthClientConfig) (*EthClient, error) { +func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, config *EthClientConfig, checker RPCRespCheck) (*EthClient, error) { if err := config.Check(); err != nil { return nil, fmt.Errorf("bad config, cannot create L1 source: %w", err) } @@ -136,6 +143,7 @@ func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, co transactionsCache: caching.NewLRUCache[common.Hash, types.Transactions](metrics, "txs", config.TransactionsCacheSize), headersCache: caching.NewLRUCache[common.Hash, eth.BlockInfo](metrics, "headers", config.HeadersCacheSize), payloadsCache: caching.NewLRUCache[common.Hash, *eth.ExecutionPayloadEnvelope](metrics, "payloads", config.PayloadsCacheSize), + respChecker: checker, }, nil } @@ -205,7 +213,7 @@ func (s *EthClient) blockCall(ctx context.Context, method string, id rpcBlockID) if block == nil { return nil, nil, ethereum.NotFound } - info, txs, err := block.Info(s.trustRPC, s.mustBePostMerge) + info, txs, err := block.Info(s.trustRPC, s.mustBePostMerge, s.respChecker) if err != nil { return nil, nil, err } @@ -226,7 +234,7 @@ func (s *EthClient) payloadCall(ctx context.Context, method string, id rpcBlockI if block == nil { return nil, ethereum.NotFound } - envelope, err := block.ExecutionPayloadEnvelope(s.trustRPC) + envelope, err := block.ExecutionPayloadEnvelope(s.trustRPC, s.respChecker) if err != nil { return nil, err } diff --git a/op-service/sources/eth_client_test.go b/op-service/sources/eth_client_test.go index fe2d9839f009..55e3d8a4b4f5 100644 --- a/op-service/sources/eth_client_test.go +++ b/op-service/sources/eth_client_test.go @@ -112,7 +112,8 @@ func TestEthClient_InfoByHash(t *testing.T) { "eth_getBlockByHash", []any{rhdr.Hash, false}).Run(func(args mock.Arguments) { *args[1].(**RPCHeader) = rhdr }).Return([]error{nil}) - s, err := NewEthClient(m, nil, nil, testEthClientConfig) + l1RpcChecker := NewL1RPCChecker() + s, err := NewEthClient(m, nil, nil, testEthClientConfig, l1RpcChecker) require.NoError(t, err) info, err := s.InfoByHash(ctx, rhdr.Hash) require.NoError(t, err) diff --git a/op-service/sources/l1_client.go b/op-service/sources/l1_client.go index d67ce1c87abd..f2321ce54353 100644 --- a/op-service/sources/l1_client.go +++ b/op-service/sources/l1_client.go @@ -65,7 +65,8 @@ type L1Client struct { // NewL1Client wraps a RPC with bindings to fetch L1 data, while logging errors, tracking metrics (optional), and caching. func NewL1Client(client client.RPC, log log.Logger, metrics caching.Metrics, config *L1ClientConfig) (*L1Client, error) { - ethClient, err := NewEthClient(client, log, metrics, &config.EthClientConfig) + l1RpcChecker := NewL1RPCChecker() + ethClient, err := NewEthClient(client, log, metrics, &config.EthClientConfig, l1RpcChecker) if err != nil { return nil, err } diff --git a/op-service/sources/l1_rpc_check.go b/op-service/sources/l1_rpc_check.go new file mode 100644 index 000000000000..0812de9d287f --- /dev/null +++ b/op-service/sources/l1_rpc_check.go @@ -0,0 +1,39 @@ +package sources + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" +) + +// Implements the RPCCheck interface for validating RPC responses +type L1RPCChecker struct { +} + +func NewL1RPCChecker() *L1RPCChecker { + return &L1RPCChecker{} +} + +func (c *L1RPCChecker) ValidateWithdrawals(withdrawals *types.Withdrawals, withdrawalsRoot *common.Hash) error { + if withdrawalsRoot != nil { + if withdrawals == nil { + return errors.New("expected withdrawals") + } + for i, w := range *withdrawals { + if w == nil { + return fmt.Errorf("block withdrawal %d is null", i) + } + } + if computed := types.DeriveSha(*withdrawals, trie.NewStackTrie(nil)); *withdrawalsRoot != computed { + return fmt.Errorf("failed to verify withdrawals list: computed %s but RPC said %s", computed, withdrawalsRoot) + } + } else { + if withdrawals != nil { + return fmt.Errorf("expected no withdrawals due to missing withdrawals-root, but got %d", len(*withdrawals)) + } + } + return nil +} diff --git a/op-service/sources/l2_client.go b/op-service/sources/l2_client.go index 078385d40510..b95822d1e8f9 100644 --- a/op-service/sources/l2_client.go +++ b/op-service/sources/l2_client.go @@ -79,7 +79,10 @@ type L2Client struct { // for fetching and caching eth.L2BlockRef values. This includes fetching an L2BlockRef by block number, label, or hash. // See: [L2BlockRefByLabel], [L2BlockRefByNumber], [L2BlockRefByHash] func NewL2Client(client client.RPC, log log.Logger, metrics caching.Metrics, config *L2ClientConfig) (*L2Client, error) { - ethClient, err := NewEthClient(client, log, metrics, &config.EthClientConfig) + + rpcChecker := NewL2RPCChecker() + + ethClient, err := NewEthClient(client, log, metrics, &config.EthClientConfig, rpcChecker) if err != nil { return nil, err } @@ -180,21 +183,29 @@ func (s *L2Client) OutputV0AtBlock(ctx context.Context, blockHash common.Hash) ( return nil, ethereum.NotFound } - proof, err := s.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, blockHash.String()) - if err != nil { - return nil, fmt.Errorf("failed to get contract proof at block %s: %w", blockHash, err) - } - if proof == nil { - return nil, fmt.Errorf("proof %w", ethereum.NotFound) - } - // make sure that the proof (including storage hash) that we retrieved is correct by verifying it against the state-root - if err := proof.Verify(head.Root()); err != nil { - return nil, fmt.Errorf("invalid withdrawal root hash, state root was %s: %w", head.Root(), err) + var messagePasserStorageRoot eth.Bytes32 + if s.rollupCfg.IsIsthmus(head.Time()) { + // If Isthmus hard fork has activated, we can get the messagePasserStorageRoot directly from the header + // instead of having to compute it from the contract storage trie. + messagePasserStorageRoot = eth.Bytes32(*head.WithdrawalsRoot()) + } else { + proof, err := s.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, blockHash.String()) + if err != nil { + return nil, fmt.Errorf("failed to get contract proof at block %s: %w", blockHash, err) + } + if proof == nil { + return nil, fmt.Errorf("proof %w", ethereum.NotFound) + } + // make sure that the proof (including storage hash) that we retrieved is correct by verifying it against the state-root + if err := proof.Verify(head.Root()); err != nil { + return nil, fmt.Errorf("invalid withdrawal root hash, state root was %s: %w", head.Root(), err) + } + messagePasserStorageRoot = eth.Bytes32(proof.StorageHash) } stateRoot := head.Root() return ð.OutputV0{ StateRoot: eth.Bytes32(stateRoot), - MessagePasserStorageRoot: eth.Bytes32(proof.StorageHash), + MessagePasserStorageRoot: eth.Bytes32(messagePasserStorageRoot), BlockHash: blockHash, }, nil } diff --git a/op-service/sources/l2_rpc_check.go b/op-service/sources/l2_rpc_check.go new file mode 100644 index 000000000000..80426a81c813 --- /dev/null +++ b/op-service/sources/l2_rpc_check.go @@ -0,0 +1,25 @@ +package sources + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// Implements the RPCCheck interface for validating RPC responses +type L2RPCChecker struct { +} + +func NewL2RPCChecker() *L2RPCChecker { + return &L2RPCChecker{} +} + +func (c *L2RPCChecker) ValidateWithdrawals(withdrawals *types.Withdrawals, withdrawalsRoot *common.Hash) error { + if withdrawalsRoot != nil { + if !(withdrawals != nil && len(*withdrawals) == 0) { + return fmt.Errorf("expected empty withdrawals, but got %d", len(*withdrawals)) + } + } + return nil +} diff --git a/op-service/sources/receipts_basic_test.go b/op-service/sources/receipts_basic_test.go index ad1ed16013ad..18a3cda2000b 100644 --- a/op-service/sources/receipts_basic_test.go +++ b/op-service/sources/receipts_basic_test.go @@ -76,7 +76,8 @@ func TestBasicRPCReceiptsFetcher_Reuse(t *testing.T) { return err } - bInfo, _, _ := block.Info(true, true) + l1RpcChecker := NewL1RPCChecker() + bInfo, _, _ := block.Info(true, true, l1RpcChecker) // 1st fetching should result in errors recs, err := rp.FetchReceipts(ctx, bInfo, txHashes) @@ -161,7 +162,8 @@ func runConcurrentFetchingTest(t *testing.T, rp ReceiptsProvider, numFetchers in } fetchResults := make(chan fetchResult, numFetchers) barrier := make(chan struct{}) - bInfo, _, _ := block.Info(true, true) + l1RpcChecker := NewL1RPCChecker() + bInfo, _, _ := block.Info(true, true, l1RpcChecker) ctx, done := context.WithTimeout(context.Background(), 10*time.Second) defer done() for i := 0; i < numFetchers; i++ { diff --git a/op-service/sources/receipts_caching_test.go b/op-service/sources/receipts_caching_test.go index e7891b9e34fd..ce065c1a1412 100644 --- a/op-service/sources/receipts_caching_test.go +++ b/op-service/sources/receipts_caching_test.go @@ -36,7 +36,8 @@ func TestCachingReceiptsProvider_Caching(t *testing.T) { Return(types.Receipts(receipts), error(nil)). Once() // receipts should be cached after first fetch - bInfo, _, _ := block.Info(true, true) + l1RpcChecker := NewL1RPCChecker() + bInfo, _, _ := block.Info(true, true, l1RpcChecker) for i := 0; i < 4; i++ { gotRecs, err := rp.FetchReceipts(ctx, bInfo, txHashes) require.NoError(t, err) diff --git a/op-service/sources/receipts_test.go b/op-service/sources/receipts_test.go index 088a3d9b22cb..e4b1425bc5e0 100644 --- a/op-service/sources/receipts_test.go +++ b/op-service/sources/receipts_test.go @@ -164,7 +164,8 @@ func (tc *ReceiptsTestCase) Run(t *testing.T) { testCfg.MethodResetDuration = 0 } logger := testlog.Logger(t, log.LevelError) - ethCl, err := NewEthClient(client.NewBaseRPCClient(cl), logger, nil, testCfg) + l2RpcChecker := NewL2RPCChecker() + ethCl, err := NewEthClient(client.NewBaseRPCClient(cl), logger, nil, testCfg, l2RpcChecker) require.NoError(t, err) defer ethCl.Close() diff --git a/op-service/sources/types.go b/op-service/sources/types.go index f337beb02058..cdcfbb2f14f8 100644 --- a/op-service/sources/types.go +++ b/op-service/sources/types.go @@ -1,7 +1,6 @@ package sources import ( - "errors" "fmt" "math/big" "strings" @@ -95,6 +94,10 @@ func (h headerInfo) ParentBeaconRoot() *common.Hash { return h.Header.ParentBeaconRoot } +func (h headerInfo) WithdrawalsRoot() *common.Hash { + return h.Header.WithdrawalsHash +} + func (h headerInfo) HeaderRLP() ([]byte, error) { return rlp.EncodeToBytes(h.Header) } @@ -216,7 +219,7 @@ type RPCBlock struct { Withdrawals *types.Withdrawals `json:"withdrawals,omitempty"` } -func (block *RPCBlock) verify() error { +func (block *RPCBlock) verify(checker RPCRespCheck) error { if computed := block.computeBlockHash(); computed != block.Hash { return fmt.Errorf("failed to verify block hash: computed %s but RPC said %s", computed, block.Hash) } @@ -228,34 +231,22 @@ func (block *RPCBlock) verify() error { if computed := types.DeriveSha(types.Transactions(block.Transactions), trie.NewStackTrie(nil)); block.TxHash != computed { return fmt.Errorf("failed to verify transactions list: computed %s but RPC said %s", computed, block.TxHash) } - if block.WithdrawalsRoot != nil { - if block.Withdrawals == nil { - return errors.New("expected withdrawals") - } - for i, w := range *block.Withdrawals { - if w == nil { - return fmt.Errorf("block withdrawal %d is null", i) - } - } - if computed := types.DeriveSha(*block.Withdrawals, trie.NewStackTrie(nil)); *block.WithdrawalsRoot != computed { - return fmt.Errorf("failed to verify withdrawals list: computed %s but RPC said %s", computed, block.WithdrawalsRoot) - } - } else { - if block.Withdrawals != nil { - return fmt.Errorf("expected no withdrawals due to missing withdrawals-root, but got %d", len(*block.Withdrawals)) - } + + if err := checker.ValidateWithdrawals(block.Withdrawals, block.WithdrawalsRoot); err != nil { + return err } + return nil } -func (block *RPCBlock) Info(trustCache bool, mustBePostMerge bool) (eth.BlockInfo, types.Transactions, error) { +func (block *RPCBlock) Info(trustCache bool, mustBePostMerge bool, checker RPCRespCheck) (eth.BlockInfo, types.Transactions, error) { if mustBePostMerge { if err := block.checkPostMerge(); err != nil { return nil, nil, err } } if !trustCache { - if err := block.verify(); err != nil { + if err := block.verify(checker); err != nil { return nil, nil, err } } @@ -269,12 +260,12 @@ func (block *RPCBlock) Info(trustCache bool, mustBePostMerge bool) (eth.BlockInf return info, block.Transactions, nil } -func (block *RPCBlock) ExecutionPayloadEnvelope(trustCache bool) (*eth.ExecutionPayloadEnvelope, error) { +func (block *RPCBlock) ExecutionPayloadEnvelope(trustCache bool, checker RPCRespCheck) (*eth.ExecutionPayloadEnvelope, error) { if err := block.checkPostMerge(); err != nil { return nil, err } if !trustCache { - if err := block.verify(); err != nil { + if err := block.verify(checker); err != nil { return nil, err } } @@ -293,23 +284,24 @@ func (block *RPCBlock) ExecutionPayloadEnvelope(trustCache bool) (*eth.Execution } payload := ð.ExecutionPayload{ - ParentHash: block.ParentHash, - FeeRecipient: block.Coinbase, - StateRoot: eth.Bytes32(block.Root), - ReceiptsRoot: eth.Bytes32(block.ReceiptHash), - LogsBloom: block.Bloom, - PrevRandao: eth.Bytes32(block.MixDigest), // mix-digest field is used for prevRandao post-merge - BlockNumber: block.Number, - GasLimit: block.GasLimit, - GasUsed: block.GasUsed, - Timestamp: block.Time, - ExtraData: eth.BytesMax32(block.Extra), - BaseFeePerGas: eth.Uint256Quantity(baseFee), - BlockHash: block.Hash, - Transactions: opaqueTxs, - Withdrawals: block.Withdrawals, - BlobGasUsed: block.BlobGasUsed, - ExcessBlobGas: block.ExcessBlobGas, + ParentHash: block.ParentHash, + FeeRecipient: block.Coinbase, + StateRoot: eth.Bytes32(block.Root), + ReceiptsRoot: eth.Bytes32(block.ReceiptHash), + LogsBloom: block.Bloom, + PrevRandao: eth.Bytes32(block.MixDigest), // mix-digest field is used for prevRandao post-merge + BlockNumber: block.Number, + GasLimit: block.GasLimit, + GasUsed: block.GasUsed, + Timestamp: block.Time, + ExtraData: eth.BytesMax32(block.Extra), + BaseFeePerGas: eth.Uint256Quantity(baseFee), + BlockHash: block.Hash, + Transactions: opaqueTxs, + Withdrawals: block.Withdrawals, + BlobGasUsed: block.BlobGasUsed, + ExcessBlobGas: block.ExcessBlobGas, + WithdrawalsRoot: block.WithdrawalsRoot, } return ð.ExecutionPayloadEnvelope{ diff --git a/op-service/sources/types_test.go b/op-service/sources/types_test.go index 6d6bad5565bf..68c0c7cf7a66 100644 --- a/op-service/sources/types_test.go +++ b/op-service/sources/types_test.go @@ -70,7 +70,8 @@ func TestBlockJSON(t *testing.T) { var block RPCBlock readJsonTestdata(t, "testdata/data/blocks/"+strings.Replace(entry.Name(), "_metadata.json", "_data.json", 1), &block) - err := block.verify() + l2RpcChecker := NewL1RPCChecker() + err := block.verify(l2RpcChecker) if metadata.Fail { require.NotNil(t, err, "expecting verification error") require.ErrorContains(t, err, metadata.Reason, "validation failed for incorrect reason") @@ -136,7 +137,8 @@ func TestBlockToExecutionPayloadIncludesEcotoneProperties(t *testing.T) { Withdrawals: &types.Withdrawals{}, } - envelope, err := block.ExecutionPayloadEnvelope(false) + l2RpcChecker := NewL1RPCChecker() + envelope, err := block.ExecutionPayloadEnvelope(false, l2RpcChecker) require.NoError(t, err) require.NotNil(t, envelope.ParentBeaconBlockRoot) diff --git a/op-service/testutils/l1info.go b/op-service/testutils/l1info.go index 8f04b71fed93..ec5f75e08012 100644 --- a/op-service/testutils/l1info.go +++ b/op-service/testutils/l1info.go @@ -30,6 +30,7 @@ type MockBlockInfo struct { InfoHeaderRLP []byte InfoParentBeaconRoot *common.Hash + InfoWithdrawalsRoot *common.Hash } func (l *MockBlockInfo) Hash() common.Hash { @@ -88,6 +89,10 @@ func (l *MockBlockInfo) ParentBeaconRoot() *common.Hash { return l.InfoParentBeaconRoot } +func (l *MockBlockInfo) WithdrawalsRoot() *common.Hash { + return l.InfoWithdrawalsRoot +} + func (l *MockBlockInfo) HeaderRLP() ([]byte, error) { if l.InfoHeaderRLP == nil { return nil, errors.New("header rlp not available") diff --git a/op-service/testutils/random.go b/op-service/testutils/random.go index a3021ad463b6..291100e307df 100644 --- a/op-service/testutils/random.go +++ b/op-service/testutils/random.go @@ -284,7 +284,8 @@ func RandomBlock(rng *rand.Rand, txCount uint64) (*types.Block, []*types.Receipt func RandomBlockPrependTxsWithTime(rng *rand.Rand, txCount int, t uint64, ptxs ...*types.Transaction) (*types.Block, []*types.Receipt) { header := RandomHeaderWithTime(rng, t) - signer := types.NewLondonSigner(big.NewInt(rng.Int63n(1000))) + chainID := big.NewInt(rng.Int63n(1000)) + signer := types.NewLondonSigner(chainID) txs := make([]*types.Transaction, 0, txCount+len(ptxs)) txs = append(txs, ptxs...) for i := 0; i < txCount; i++ { @@ -302,7 +303,7 @@ func RandomBlockPrependTxsWithTime(rng *rand.Rand, txCount int, t uint64, ptxs . body := types.Body{ Transactions: txs, } - block := types.NewBlock(header, &body, receipts, trie.NewStackTrie(nil)) + block := types.NewBlock(header, &body, receipts, trie.NewStackTrie(nil), ¶ms.ChainConfig{ChainID: chainID}) logIndex := uint(0) for i, r := range receipts { r.BlockHash = block.Hash() diff --git a/ops/docker/ci-builder/Dockerfile b/ops/docker/ci-builder/Dockerfile index a1eb71b4795e..2107fbbab67c 100644 --- a/ops/docker/ci-builder/Dockerfile +++ b/ops/docker/ci-builder/Dockerfile @@ -96,7 +96,7 @@ COPY ./versions.json ./versions.json RUN /bin/sh -c set -eux; \ apt-get update; \ - apt-get install -y --no-install-recommends bash curl openssh-client git build-essential ca-certificates jq gnupg binutils-mips-linux-gnu python3 python3-pip python3-setuptools; \ + apt-get install -y --no-install-recommends bash curl openssh-client git build-essential ca-certificates jq gnupg binutils-mips-linux-gnu python3 python3-pip python3-setuptools mercurial make binutils bison gcc bsdmainutils; \ mkdir -p /etc/apt/keyrings; \ curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg; \ chmod a+r /etc/apt/keyrings/docker.gpg; \ @@ -107,7 +107,11 @@ RUN /bin/sh -c set -eux; \ pip install capstone pyelftools; \ pip install semgrep==$(jq -r .semgrep < versions.json); \ curl -fLSs https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/master/install.sh | bash; \ - apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash; \ + bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer); \ + nvm install 18.12.1; + +RUN apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ rm -rf /var/lib/apt/lists/*; \ rm -rf /root/.cache/pip; diff --git a/packages/contracts-bedrock/README.md b/packages/contracts-bedrock/README.md index a9f1dbeeaa5f..5421a970a997 100644 --- a/packages/contracts-bedrock/README.md +++ b/packages/contracts-bedrock/README.md @@ -139,8 +139,8 @@ Use the env var `DEPLOY_CONFIG_PATH` to use a particular deploy config file at r The script will read the latest active fork from the deploy config and the L2 genesis allocs generated will be compatible with this fork. The automatically detected fork can be overwritten by setting the environment variable `FORK` -either to the lower-case fork name (currently `delta`, `ecotone`, `fjord`, `granite`, or `holocene`) or to `latest`, -which will select the latest fork available (currently `holocene`). +either to the lower-case fork name (currently `delta`, `ecotone`, `fjord`, `granite`, `holocene` or `isthmus`) or to `latest`, +which will select the latest fork available (currently `isthmus`). By default, the script will dump the L2 genesis allocs of the detected or selected fork only, to the file at `STATE_DUMP_PATH`. The optional environment variable `OUTPUT_MODE` allows to modify this behavior by setting it to one of the following values: diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 0f681185a551..befa1dbfbda5 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -182,6 +182,9 @@ contract L2Genesis is Deployer { if (writeForkGenesisAllocs(_fork, Fork.HOLOCENE, _mode)) { return; } + if (writeForkGenesisAllocs(_fork, Fork.ISTHMUS, _mode)) { + return; + } } function writeForkGenesisAllocs(Fork _latest, Fork _current, OutputMode _mode) internal returns (bool isLatest_) { diff --git a/packages/contracts-bedrock/scripts/libraries/Config.sol b/packages/contracts-bedrock/scripts/libraries/Config.sol index 18084761e807..4aca8bc31e3c 100644 --- a/packages/contracts-bedrock/scripts/libraries/Config.sol +++ b/packages/contracts-bedrock/scripts/libraries/Config.sol @@ -34,10 +34,11 @@ enum Fork { ECOTONE, FJORD, GRANITE, - HOLOCENE + HOLOCENE, + ISTHMUS } -Fork constant LATEST_FORK = Fork.HOLOCENE; +Fork constant LATEST_FORK = Fork.ISTHMUS; library ForkUtils { function toString(Fork _fork) internal pure returns (string memory) { @@ -53,6 +54,8 @@ library ForkUtils { return "granite"; } else if (_fork == Fork.HOLOCENE) { return "holocene"; + } else if (_fork == Fork.ISTHMUS) { + return "isthmus"; } else { return "unknown"; } @@ -168,6 +171,8 @@ library Config { return Fork.GRANITE; } else if (forkHash == keccak256(bytes("holocene"))) { return Fork.HOLOCENE; + } else if (forkHash == keccak256(bytes("isthmus"))) { + return Fork.ISTHMUS; } else { revert(string.concat("Config: unknown fork: ", forkStr)); } diff --git a/versions.json b/versions.json index 5a2ae52b57f0..b1e4084ea236 100644 --- a/versions.json +++ b/versions.json @@ -10,5 +10,5 @@ "kontrol": "1.0.53", "just": "1.34.0", "binary_signer": "1.0.4", - "semgrep": "1.90.0" + "semgrep": "1.96.0" }