diff --git a/.gitignore b/.gitignore index 369a28572..5e39098e6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ contracts/mainnet.json .env # logs -*.log \ No newline at end of file +*.log + +# mpt-switch-test (local testing only) +ops/mpt-switch-test \ No newline at end of file diff --git a/Makefile b/Makefile index 494ed9049..afde54dbc 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ ################## update dependencies #################### -ETHEREUM_SUBMODULE_COMMIT_OR_TAG := morph-v2.1.0 -ETHEREUM_TARGET_VERSION := v1.10.14-0.20251219060125-03910bc750a2 +ETHEREUM_SUBMODULE_COMMIT_OR_TAG := morph-v2.1.2 +ETHEREUM_TARGET_VERSION := morph-v2.1.2 TENDERMINT_TARGET_VERSION := v0.3.3 + ETHEREUM_MODULE_NAME := github.com/morph-l2/go-ethereum TENDERMINT_MODULE_NAME := github.com/morph-l2/tendermint diff --git a/bindings/bindings/l1sequencer.go b/bindings/bindings/l1sequencer.go new file mode 100644 index 000000000..80110f035 --- /dev/null +++ b/bindings/bindings/l1sequencer.go @@ -0,0 +1,820 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/morph-l2/go-ethereum" + "github.com/morph-l2/go-ethereum/accounts/abi" + "github.com/morph-l2/go-ethereum/accounts/abi/bind" + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/types" + "github.com/morph-l2/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// L1SequencerMetaData contains all meta data concerning the L1Sequencer contract. +var L1SequencerMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"function\",\"name\":\"getSequencer\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"sequencer\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"updateSequencer\",\"inputs\":[{\"name\":\"newSequencer\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"Initialized\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"uint8\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SequencerUpdated\",\"inputs\":[{\"name\":\"oldSequencer\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newSequencer\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false}]", + Bin: "0x608060405234801561000f575f80fd5b5061081a8061001d5f395ff3fe608060405234801561000f575f80fd5b506004361061007a575f3560e01c8063715018a611610058578063715018a6146100f65780638da5cb5b146100fe578063c4d66de81461011c578063f2fde38b1461012f575f80fd5b806343ae20a31461007e5780634d96a90a146100935780635c1bba38146100d6575b5f80fd5b61009161008c3660046107d3565b610142565b005b60655473ffffffffffffffffffffffffffffffffffffffff165b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b6065546100ad9073ffffffffffffffffffffffffffffffffffffffff1681565b6100916102c7565b60335473ffffffffffffffffffffffffffffffffffffffff166100ad565b61009161012a3660046107d3565b6102da565b61009161013d3660046107d3565b6104ed565b61014a6105a4565b73ffffffffffffffffffffffffffffffffffffffff81166101cc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601160248201527f696e76616c69642073657175656e63657200000000000000000000000000000060448201526064015b60405180910390fd5b60655473ffffffffffffffffffffffffffffffffffffffff90811690821603610251576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600e60248201527f73616d652073657175656e63657200000000000000000000000000000000000060448201526064016101c3565b6065805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681179093556040519116919082907fcd58b762453bd126b48db83f2cecd464f5281dd7e5e6824b528c09d0482984d6905f90a35050565b6102cf6105a4565b6102d85f610625565b565b5f54610100900460ff16158080156102f857505f54600160ff909116105b806103115750303b15801561031157505f5460ff166001145b61039d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a656400000000000000000000000000000000000060648201526084016101c3565b5f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016600117905580156103f9575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790555b73ffffffffffffffffffffffffffffffffffffffff8216610476576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600d60248201527f696e76616c6964206f776e65720000000000000000000000000000000000000060448201526064016101c3565b61047e61069b565b61048782610625565b80156104e9575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b5050565b6104f56105a4565b73ffffffffffffffffffffffffffffffffffffffff8116610598576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084016101c3565b6105a181610625565b50565b60335473ffffffffffffffffffffffffffffffffffffffff1633146102d8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016101c3565b6033805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f54610100900460ff16610731576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e6700000000000000000000000000000000000000000060648201526084016101c3565b6102d85f54610100900460ff166107ca576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e6700000000000000000000000000000000000000000060648201526084016101c3565b6102d833610625565b5f602082840312156107e3575f80fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610806575f80fd5b939250505056fea164736f6c6343000818000a", +} + +// L1SequencerABI is the input ABI used to generate the binding from. +// Deprecated: Use L1SequencerMetaData.ABI instead. +var L1SequencerABI = L1SequencerMetaData.ABI + +// L1SequencerBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use L1SequencerMetaData.Bin instead. +var L1SequencerBin = L1SequencerMetaData.Bin + +// DeployL1Sequencer deploys a new Ethereum contract, binding an instance of L1Sequencer to it. +func DeployL1Sequencer(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *L1Sequencer, error) { + parsed, err := L1SequencerMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(L1SequencerBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &L1Sequencer{L1SequencerCaller: L1SequencerCaller{contract: contract}, L1SequencerTransactor: L1SequencerTransactor{contract: contract}, L1SequencerFilterer: L1SequencerFilterer{contract: contract}}, nil +} + +// L1Sequencer is an auto generated Go binding around an Ethereum contract. +type L1Sequencer struct { + L1SequencerCaller // Read-only binding to the contract + L1SequencerTransactor // Write-only binding to the contract + L1SequencerFilterer // Log filterer for contract events +} + +// L1SequencerCaller is an auto generated read-only Go binding around an Ethereum contract. +type L1SequencerCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// L1SequencerTransactor is an auto generated write-only Go binding around an Ethereum contract. +type L1SequencerTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// L1SequencerFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type L1SequencerFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// L1SequencerSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type L1SequencerSession struct { + Contract *L1Sequencer // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// L1SequencerCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type L1SequencerCallerSession struct { + Contract *L1SequencerCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// L1SequencerTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type L1SequencerTransactorSession struct { + Contract *L1SequencerTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// L1SequencerRaw is an auto generated low-level Go binding around an Ethereum contract. +type L1SequencerRaw struct { + Contract *L1Sequencer // Generic contract binding to access the raw methods on +} + +// L1SequencerCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type L1SequencerCallerRaw struct { + Contract *L1SequencerCaller // Generic read-only contract binding to access the raw methods on +} + +// L1SequencerTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type L1SequencerTransactorRaw struct { + Contract *L1SequencerTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewL1Sequencer creates a new instance of L1Sequencer, bound to a specific deployed contract. +func NewL1Sequencer(address common.Address, backend bind.ContractBackend) (*L1Sequencer, error) { + contract, err := bindL1Sequencer(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &L1Sequencer{L1SequencerCaller: L1SequencerCaller{contract: contract}, L1SequencerTransactor: L1SequencerTransactor{contract: contract}, L1SequencerFilterer: L1SequencerFilterer{contract: contract}}, nil +} + +// NewL1SequencerCaller creates a new read-only instance of L1Sequencer, bound to a specific deployed contract. +func NewL1SequencerCaller(address common.Address, caller bind.ContractCaller) (*L1SequencerCaller, error) { + contract, err := bindL1Sequencer(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &L1SequencerCaller{contract: contract}, nil +} + +// NewL1SequencerTransactor creates a new write-only instance of L1Sequencer, bound to a specific deployed contract. +func NewL1SequencerTransactor(address common.Address, transactor bind.ContractTransactor) (*L1SequencerTransactor, error) { + contract, err := bindL1Sequencer(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &L1SequencerTransactor{contract: contract}, nil +} + +// NewL1SequencerFilterer creates a new log filterer instance of L1Sequencer, bound to a specific deployed contract. +func NewL1SequencerFilterer(address common.Address, filterer bind.ContractFilterer) (*L1SequencerFilterer, error) { + contract, err := bindL1Sequencer(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &L1SequencerFilterer{contract: contract}, nil +} + +// bindL1Sequencer binds a generic wrapper to an already deployed contract. +func bindL1Sequencer(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := L1SequencerMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_L1Sequencer *L1SequencerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _L1Sequencer.Contract.L1SequencerCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_L1Sequencer *L1SequencerRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _L1Sequencer.Contract.L1SequencerTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_L1Sequencer *L1SequencerRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _L1Sequencer.Contract.L1SequencerTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_L1Sequencer *L1SequencerCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _L1Sequencer.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_L1Sequencer *L1SequencerTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _L1Sequencer.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_L1Sequencer *L1SequencerTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _L1Sequencer.Contract.contract.Transact(opts, method, params...) +} + +// GetSequencer is a free data retrieval call binding the contract method 0x4d96a90a. +// +// Solidity: function getSequencer() view returns(address) +func (_L1Sequencer *L1SequencerCaller) GetSequencer(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _L1Sequencer.contract.Call(opts, &out, "getSequencer") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetSequencer is a free data retrieval call binding the contract method 0x4d96a90a. +// +// Solidity: function getSequencer() view returns(address) +func (_L1Sequencer *L1SequencerSession) GetSequencer() (common.Address, error) { + return _L1Sequencer.Contract.GetSequencer(&_L1Sequencer.CallOpts) +} + +// GetSequencer is a free data retrieval call binding the contract method 0x4d96a90a. +// +// Solidity: function getSequencer() view returns(address) +func (_L1Sequencer *L1SequencerCallerSession) GetSequencer() (common.Address, error) { + return _L1Sequencer.Contract.GetSequencer(&_L1Sequencer.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_L1Sequencer *L1SequencerCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _L1Sequencer.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_L1Sequencer *L1SequencerSession) Owner() (common.Address, error) { + return _L1Sequencer.Contract.Owner(&_L1Sequencer.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_L1Sequencer *L1SequencerCallerSession) Owner() (common.Address, error) { + return _L1Sequencer.Contract.Owner(&_L1Sequencer.CallOpts) +} + +// Sequencer is a free data retrieval call binding the contract method 0x5c1bba38. +// +// Solidity: function sequencer() view returns(address) +func (_L1Sequencer *L1SequencerCaller) Sequencer(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _L1Sequencer.contract.Call(opts, &out, "sequencer") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Sequencer is a free data retrieval call binding the contract method 0x5c1bba38. +// +// Solidity: function sequencer() view returns(address) +func (_L1Sequencer *L1SequencerSession) Sequencer() (common.Address, error) { + return _L1Sequencer.Contract.Sequencer(&_L1Sequencer.CallOpts) +} + +// Sequencer is a free data retrieval call binding the contract method 0x5c1bba38. +// +// Solidity: function sequencer() view returns(address) +func (_L1Sequencer *L1SequencerCallerSession) Sequencer() (common.Address, error) { + return _L1Sequencer.Contract.Sequencer(&_L1Sequencer.CallOpts) +} + +// Initialize is a paid mutator transaction binding the contract method 0xc4d66de8. +// +// Solidity: function initialize(address _owner) returns() +func (_L1Sequencer *L1SequencerTransactor) Initialize(opts *bind.TransactOpts, _owner common.Address) (*types.Transaction, error) { + return _L1Sequencer.contract.Transact(opts, "initialize", _owner) +} + +// Initialize is a paid mutator transaction binding the contract method 0xc4d66de8. +// +// Solidity: function initialize(address _owner) returns() +func (_L1Sequencer *L1SequencerSession) Initialize(_owner common.Address) (*types.Transaction, error) { + return _L1Sequencer.Contract.Initialize(&_L1Sequencer.TransactOpts, _owner) +} + +// Initialize is a paid mutator transaction binding the contract method 0xc4d66de8. +// +// Solidity: function initialize(address _owner) returns() +func (_L1Sequencer *L1SequencerTransactorSession) Initialize(_owner common.Address) (*types.Transaction, error) { + return _L1Sequencer.Contract.Initialize(&_L1Sequencer.TransactOpts, _owner) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_L1Sequencer *L1SequencerTransactor) RenounceOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _L1Sequencer.contract.Transact(opts, "renounceOwnership") +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_L1Sequencer *L1SequencerSession) RenounceOwnership() (*types.Transaction, error) { + return _L1Sequencer.Contract.RenounceOwnership(&_L1Sequencer.TransactOpts) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_L1Sequencer *L1SequencerTransactorSession) RenounceOwnership() (*types.Transaction, error) { + return _L1Sequencer.Contract.RenounceOwnership(&_L1Sequencer.TransactOpts) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_L1Sequencer *L1SequencerTransactor) TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) { + return _L1Sequencer.contract.Transact(opts, "transferOwnership", newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_L1Sequencer *L1SequencerSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _L1Sequencer.Contract.TransferOwnership(&_L1Sequencer.TransactOpts, newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_L1Sequencer *L1SequencerTransactorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _L1Sequencer.Contract.TransferOwnership(&_L1Sequencer.TransactOpts, newOwner) +} + +// UpdateSequencer is a paid mutator transaction binding the contract method 0x43ae20a3. +// +// Solidity: function updateSequencer(address newSequencer) returns() +func (_L1Sequencer *L1SequencerTransactor) UpdateSequencer(opts *bind.TransactOpts, newSequencer common.Address) (*types.Transaction, error) { + return _L1Sequencer.contract.Transact(opts, "updateSequencer", newSequencer) +} + +// UpdateSequencer is a paid mutator transaction binding the contract method 0x43ae20a3. +// +// Solidity: function updateSequencer(address newSequencer) returns() +func (_L1Sequencer *L1SequencerSession) UpdateSequencer(newSequencer common.Address) (*types.Transaction, error) { + return _L1Sequencer.Contract.UpdateSequencer(&_L1Sequencer.TransactOpts, newSequencer) +} + +// UpdateSequencer is a paid mutator transaction binding the contract method 0x43ae20a3. +// +// Solidity: function updateSequencer(address newSequencer) returns() +func (_L1Sequencer *L1SequencerTransactorSession) UpdateSequencer(newSequencer common.Address) (*types.Transaction, error) { + return _L1Sequencer.Contract.UpdateSequencer(&_L1Sequencer.TransactOpts, newSequencer) +} + +// L1SequencerInitializedIterator is returned from FilterInitialized and is used to iterate over the raw logs and unpacked data for Initialized events raised by the L1Sequencer contract. +type L1SequencerInitializedIterator struct { + Event *L1SequencerInitialized // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *L1SequencerInitializedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(L1SequencerInitialized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(L1SequencerInitialized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *L1SequencerInitializedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *L1SequencerInitializedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// L1SequencerInitialized represents a Initialized event raised by the L1Sequencer contract. +type L1SequencerInitialized struct { + Version uint8 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInitialized is a free log retrieval operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_L1Sequencer *L1SequencerFilterer) FilterInitialized(opts *bind.FilterOpts) (*L1SequencerInitializedIterator, error) { + + logs, sub, err := _L1Sequencer.contract.FilterLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return &L1SequencerInitializedIterator{contract: _L1Sequencer.contract, event: "Initialized", logs: logs, sub: sub}, nil +} + +// WatchInitialized is a free log subscription operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_L1Sequencer *L1SequencerFilterer) WatchInitialized(opts *bind.WatchOpts, sink chan<- *L1SequencerInitialized) (event.Subscription, error) { + + logs, sub, err := _L1Sequencer.contract.WatchLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(L1SequencerInitialized) + if err := _L1Sequencer.contract.UnpackLog(event, "Initialized", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseInitialized is a log parse operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_L1Sequencer *L1SequencerFilterer) ParseInitialized(log types.Log) (*L1SequencerInitialized, error) { + event := new(L1SequencerInitialized) + if err := _L1Sequencer.contract.UnpackLog(event, "Initialized", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// L1SequencerOwnershipTransferredIterator is returned from FilterOwnershipTransferred and is used to iterate over the raw logs and unpacked data for OwnershipTransferred events raised by the L1Sequencer contract. +type L1SequencerOwnershipTransferredIterator struct { + Event *L1SequencerOwnershipTransferred // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *L1SequencerOwnershipTransferredIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(L1SequencerOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(L1SequencerOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *L1SequencerOwnershipTransferredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *L1SequencerOwnershipTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// L1SequencerOwnershipTransferred represents a OwnershipTransferred event raised by the L1Sequencer contract. +type L1SequencerOwnershipTransferred struct { + PreviousOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferred is a free log retrieval operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_L1Sequencer *L1SequencerFilterer) FilterOwnershipTransferred(opts *bind.FilterOpts, previousOwner []common.Address, newOwner []common.Address) (*L1SequencerOwnershipTransferredIterator, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _L1Sequencer.contract.FilterLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &L1SequencerOwnershipTransferredIterator{contract: _L1Sequencer.contract, event: "OwnershipTransferred", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferred is a free log subscription operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_L1Sequencer *L1SequencerFilterer) WatchOwnershipTransferred(opts *bind.WatchOpts, sink chan<- *L1SequencerOwnershipTransferred, previousOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _L1Sequencer.contract.WatchLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(L1SequencerOwnershipTransferred) + if err := _L1Sequencer.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferred is a log parse operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_L1Sequencer *L1SequencerFilterer) ParseOwnershipTransferred(log types.Log) (*L1SequencerOwnershipTransferred, error) { + event := new(L1SequencerOwnershipTransferred) + if err := _L1Sequencer.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// L1SequencerSequencerUpdatedIterator is returned from FilterSequencerUpdated and is used to iterate over the raw logs and unpacked data for SequencerUpdated events raised by the L1Sequencer contract. +type L1SequencerSequencerUpdatedIterator struct { + Event *L1SequencerSequencerUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *L1SequencerSequencerUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(L1SequencerSequencerUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(L1SequencerSequencerUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *L1SequencerSequencerUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *L1SequencerSequencerUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// L1SequencerSequencerUpdated represents a SequencerUpdated event raised by the L1Sequencer contract. +type L1SequencerSequencerUpdated struct { + OldSequencer common.Address + NewSequencer common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSequencerUpdated is a free log retrieval operation binding the contract event 0xcd58b762453bd126b48db83f2cecd464f5281dd7e5e6824b528c09d0482984d6. +// +// Solidity: event SequencerUpdated(address indexed oldSequencer, address indexed newSequencer) +func (_L1Sequencer *L1SequencerFilterer) FilterSequencerUpdated(opts *bind.FilterOpts, oldSequencer []common.Address, newSequencer []common.Address) (*L1SequencerSequencerUpdatedIterator, error) { + + var oldSequencerRule []interface{} + for _, oldSequencerItem := range oldSequencer { + oldSequencerRule = append(oldSequencerRule, oldSequencerItem) + } + var newSequencerRule []interface{} + for _, newSequencerItem := range newSequencer { + newSequencerRule = append(newSequencerRule, newSequencerItem) + } + + logs, sub, err := _L1Sequencer.contract.FilterLogs(opts, "SequencerUpdated", oldSequencerRule, newSequencerRule) + if err != nil { + return nil, err + } + return &L1SequencerSequencerUpdatedIterator{contract: _L1Sequencer.contract, event: "SequencerUpdated", logs: logs, sub: sub}, nil +} + +// WatchSequencerUpdated is a free log subscription operation binding the contract event 0xcd58b762453bd126b48db83f2cecd464f5281dd7e5e6824b528c09d0482984d6. +// +// Solidity: event SequencerUpdated(address indexed oldSequencer, address indexed newSequencer) +func (_L1Sequencer *L1SequencerFilterer) WatchSequencerUpdated(opts *bind.WatchOpts, sink chan<- *L1SequencerSequencerUpdated, oldSequencer []common.Address, newSequencer []common.Address) (event.Subscription, error) { + + var oldSequencerRule []interface{} + for _, oldSequencerItem := range oldSequencer { + oldSequencerRule = append(oldSequencerRule, oldSequencerItem) + } + var newSequencerRule []interface{} + for _, newSequencerItem := range newSequencer { + newSequencerRule = append(newSequencerRule, newSequencerItem) + } + + logs, sub, err := _L1Sequencer.contract.WatchLogs(opts, "SequencerUpdated", oldSequencerRule, newSequencerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(L1SequencerSequencerUpdated) + if err := _L1Sequencer.contract.UnpackLog(event, "SequencerUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSequencerUpdated is a log parse operation binding the contract event 0xcd58b762453bd126b48db83f2cecd464f5281dd7e5e6824b528c09d0482984d6. +// +// Solidity: event SequencerUpdated(address indexed oldSequencer, address indexed newSequencer) +func (_L1Sequencer *L1SequencerFilterer) ParseSequencerUpdated(log types.Log) (*L1SequencerSequencerUpdated, error) { + event := new(L1SequencerSequencerUpdated) + if err := _L1Sequencer.contract.UnpackLog(event, "SequencerUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/bindings/go.mod b/bindings/go.mod index e5d397882..a509930b7 100644 --- a/bindings/go.mod +++ b/bindings/go.mod @@ -4,7 +4,7 @@ go 1.24.0 replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.3 -require github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 +require github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 require ( github.com/VictoriaMetrics/fastcache v1.12.2 // indirect @@ -27,11 +27,9 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/holiman/uint256 v1.2.4 // indirect github.com/iden3/go-iden3-crypto v0.0.16 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/tsdb v0.10.0 // indirect - github.com/rivo/uniseg v0.4.3 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/scroll-tech/zktrie v0.8.4 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect diff --git a/bindings/go.sum b/bindings/go.sum index a479ed434..bb71a7769 100644 --- a/bindings/go.sum +++ b/bindings/go.sum @@ -103,16 +103,16 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 h1:FUv9gtnvF+1AVrkoNGYbVOesi7E+STjdfD2mcqVaEY0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 h1:A8eygErKU6WKMipGWIemzwLeYkIGLd9yb/Ry3x+J9PQ= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -143,9 +143,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/scroll-tech/zktrie v0.8.4 h1:UagmnZ4Z3ITCk+aUq9NQZJNAwnWl4gSxsLb2Nl7IgRE= diff --git a/contracts/contracts/l1/L1Sequencer.sol b/contracts/contracts/l1/L1Sequencer.sol new file mode 100644 index 000000000..3a46768bd --- /dev/null +++ b/contracts/contracts/l1/L1Sequencer.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.24; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/// @title L1Sequencer +/// @notice L1 contract for managing the sequencer address. +/// The sequencer address can be updated by the owner (multisig recommended). +contract L1Sequencer is OwnableUpgradeable { + // ============ Storage ============ + + /// @notice Current sequencer address + address public sequencer; + + // ============ Events ============ + + /// @notice Emitted when sequencer is updated + event SequencerUpdated(address indexed oldSequencer, address indexed newSequencer); + + // ============ Initializer ============ + + /// @notice Initialize the contract + /// @param _owner Contract owner (multisig recommended) + /// @param _initialSequencer Initial sequencer address (can be address(0) to set later) + function initialize(address _owner, address _initialSequencer) external initializer { + require(_owner != address(0), "invalid owner"); + + __Ownable_init(); + _transferOwnership(_owner); + + // Set initial sequencer if provided + if (_initialSequencer != address(0)) { + sequencer = _initialSequencer; + emit SequencerUpdated(address(0), _initialSequencer); + } + } + + // ============ Admin Functions ============ + + /// @notice Update sequencer address (takes effect immediately) + /// @param newSequencer New sequencer address + function updateSequencer(address newSequencer) external onlyOwner { + require(newSequencer != address(0), "invalid sequencer"); + require(newSequencer != sequencer, "same sequencer"); + + address oldSequencer = sequencer; + sequencer = newSequencer; + + emit SequencerUpdated(oldSequencer, newSequencer); + } + + // ============ View Functions ============ + + /// @notice Get current sequencer address + function getSequencer() external view returns (address) { + return sequencer; + } +} diff --git a/contracts/contracts/l1/rollup/Rollup.sol b/contracts/contracts/l1/rollup/Rollup.sol index 2148bb60e..68471420e 100644 --- a/contracts/contracts/l1/rollup/Rollup.sol +++ b/contracts/contracts/l1/rollup/Rollup.sol @@ -668,6 +668,17 @@ contract Rollup is IRollup, OwnableUpgradeable, PausableUpgradeable { return batchDataStore[batchIndex].finalizeTimestamp > block.timestamp; } + /// @dev proveCommittedBatchState verifies the ZK proof for a committed batch. + function proveCommittedBatchState(bytes calldata _batchHeader, bytes calldata _batchProof) public view { + // get batch data from batch header + (uint256 memPtr, bytes32 _batchHash) = _loadBatchHeader(_batchHeader); + // check batch hash + uint256 _batchIndex = BatchHeaderCodecV0.getBatchIndex(memPtr); + require(committedBatches[_batchIndex] == _batchHash, "incorrect batch hash"); + + _verifyProof(memPtr, _batchProof); + } + /********************** * Internal Functions * **********************/ diff --git a/contracts/contracts/test/Rollup.t.sol b/contracts/contracts/test/Rollup.t.sol index c7fd2ae33..f6744f911 100644 --- a/contracts/contracts/test/Rollup.t.sol +++ b/contracts/contracts/test/Rollup.t.sol @@ -587,6 +587,48 @@ contract RollupCommitBatchWithProofTest is L1MessageBaseTest { assertEq(rollup.lastCommittedBatchIndex(), 1); } + + /// @notice Test: proveCommittedBatchState proves the state of a committed batch. + function test_proveCommittedBatchState() public { + _mockVerifierCall(); + _mockMessageQueueStalled(); + + // Warp time to simulate stall (> rollupDelayPeriod) + hevm.warp(block.timestamp + 7200); + + bytes32 prevStateRoot = bytes32(uint256(1)); + bytes32 postStateRoot = bytes32(uint256(2)); + bytes32 withdrawalRoot = getTreeRoot(); + + IRollup.BatchDataInput memory batchDataInput = IRollup.BatchDataInput({ + version: 0, + parentBatchHeader: batchHeader0, + lastBlockNumber: 1, + numL1Messages: 0, + prevStateRoot: prevStateRoot, + postStateRoot: postStateRoot, + withdrawalRoot: withdrawalRoot + }); + + bytes memory batchHeader1 = _createMatchingBatchHeader(1, 0, prevStateRoot, postStateRoot, withdrawalRoot); + + hevm.prank(alice); + rollup.commitBatchWithProof( + batchDataInput, + batchSignatureInput, + batchHeader1, + hex"deadbeef" // Non-empty proof required + ); + // Verify batch was committed (finalization happens separately via finalizeBatch) + assertEq(rollup.lastCommittedBatchIndex(), 1); + // finalizeTimestamp is set to block.timestamp, allowing immediate finalization + // but lastFinalizedBatchIndex is not updated until finalizeBatch is called + assertEq(rollup.lastFinalizedBatchIndex(), 0); + + bytes + memory proof = hex"ffea2d2e0a0f124b2bc411325821f76e40978468a0892f57f7de20c99a4fe762dbbdfbc929f5b68b89bdecd40143f36782957e6b818fb761be749d283b16daab432f249b0ddb5537d6b7a8275505a9213eb15db1ddb095bada27a03b6f9c4edb5fd8f7110c7438abff87fed406080f3a103ad45c41fdf89542ff037adfe314a39e5688931da3a1f52f19a3a942cd36dace89340bcff9a18420596f6a06e5d14e5230baba07d1fb0a5bd20beb246875f3ad238dc040c347b10795fff4d6a92ac057c61d672f1d17bbab356c0ade3c419fb1d991f879c2d4fa02b8c4c6ad2d78a8338e9c101e7ea8eb9f1fe61a698598959eed8a33919226a623aa1cd5881d2db0d7f1811921abbb34d035056eea6a70c6079d13de1f67ccf4b2c54398a30cbbf490c2206c111c5e8734d5ef6574d1520f0e68acf482d40f11ac18a8316cb524b3b4a8adb8178e4333eb170bc3a0abfe3d69ced1c5f5b781be739ad2c45b8971455681bcd625348a050a4c5ebc469bf5f8ddb95d541baea4b71e9430452e37c084b2d0ab3c104dfe6dfab22413bf9ea4b2009b5248ec471432ab7464546ffe15033df99dba08077a60ae40633c2a5511faba9e00683497059265ab8114546729007d1e50670dc0a2cecf23cbe58417b2cf51afbf3fd8da63d936d8a92b6e9203c89665a3f40447bc5f08016739567287e5e824c164dc0c7dea8e95eddda27c4966c4d6674516d1cf84241a61b7fadbc432ce6253be085ab86771bf573aa6e506b4c98254d0192a1924297c2e29dfa5b19d99a8ff4ca3975803020f6f46c3200e09d2fb47282f5ced534b0301e5f7501aa56dc77d534c25849d7165efdba546883dda634db60f2d4a9ab608827d63a37020466318f704e30ba4223106a8092f052926714a7a2819a9afb7fb970a9a6d3058cb01ecd4d82e2e20f8996b0995818ae9c3ca815008fd01cdf42f187f723e6965c3c5ee972c9bbef7e7633776cf1af533460565bb256c5c9c6c1c50b63785daabe702d838308659e02c338ba5b47cd0508000eb4426bc76e4760d380bf9d1eb28e1dbcc9cd3a562a6b35ead2d434bce87657ab0ab2cfac2e3c410132c4bef39559dc853bfbf8319447dd365d0a6f52277046b1fdf284f27e626f4f86165eaea9b41bd4bf348325975bf3685a041c4740d300a4222063a6038b5da62c56052c5ddd1d845b51ae2782cec83fcae3966d7f4692d34fe00000000000000000000000000000000000000000000000000000000"; + rollup.proveCommittedBatchState(batchHeader1, proof); + } } contract RollupCommitBatchTest is L1MessageBaseTest { diff --git a/contracts/deploy/013-DeployProxys.ts b/contracts/deploy/013-DeployProxys.ts index 24545613a..6b3a4a5ce 100644 --- a/contracts/deploy/013-DeployProxys.ts +++ b/contracts/deploy/013-DeployProxys.ts @@ -48,6 +48,7 @@ export const deployContractProxies = async ( const RollupProxyStorageName = ProxyStorageName.RollupProxyStorageName const L1StakingProxyStorageName = ProxyStorageName.L1StakingProxyStorageName + const L1SequencerProxyStorageName = ProxyStorageName.L1SequencerProxyStorageName const L1GatewayRouterProxyStorageName = ProxyStorageName.L1GatewayRouterProxyStorageName const L1ETHGatewayProxyStorageName = ProxyStorageName.L1ETHGatewayProxyStorageName @@ -112,6 +113,13 @@ export const deployContractProxies = async ( return err } + // ************************ sequencer contracts deploy ************************ + // L1SequencerProxy deploy + err = await deployContractProxyByStorageName(hre, path, deployer, L1SequencerProxyStorageName) + if (err != "") { + return err + } + // ************************ rollup contracts deploy ************************ // RollupProxy deploy err = await deployContractProxyByStorageName(hre, path, deployer, RollupProxyStorageName) @@ -274,6 +282,7 @@ export const deployContractProxiesConcurrently = async ( ProxyStorageName.L1CrossDomainMessengerProxyStorageName, ProxyStorageName.L1MessageQueueWithGasPriceOracleProxyStorageName, ProxyStorageName.L1StakingProxyStorageName, + ProxyStorageName.L1SequencerProxyStorageName, ProxyStorageName.RollupProxyStorageName, ProxyStorageName.L1GatewayRouterProxyStorageName, ProxyStorageName.L1ETHGatewayProxyStorageName, diff --git a/contracts/deploy/014-DeployImpls.ts b/contracts/deploy/014-DeployImpls.ts index ed0653706..b32613018 100644 --- a/contracts/deploy/014-DeployImpls.ts +++ b/contracts/deploy/014-DeployImpls.ts @@ -122,6 +122,11 @@ export const deployContractImplsConcurrently = async ( deployPromises.push(deployContract(L1StakingFactoryName, StakingImplStorageName, [L1CrossDomainMessengerProxyAddress])) + // L1Sequencer deploy (no constructor args) + const L1SequencerFactoryName = ContractFactoryName.L1Sequencer + const L1SequencerImplStorageName = ImplStorageName.L1SequencerStorageName + deployPromises.push(deployContract(L1SequencerFactoryName, L1SequencerImplStorageName)) + const results = await Promise.all(deployPromises) for (const result of results) { @@ -382,6 +387,21 @@ export const deployContractImpls = async ( return err } + // ************************ sequencer contracts deploy ************************ + // L1Sequencer deploy + const L1SequencerFactoryName = ContractFactoryName.L1Sequencer + const L1SequencerImplStorageName = ImplStorageName.L1SequencerStorageName + Factory = await hre.ethers.getContractFactory(L1SequencerFactoryName) + contract = await Factory.deploy() + await contract.deployed() + console.log("%s=%s ; TX_HASH: %s", L1SequencerImplStorageName, contract.address.toLocaleLowerCase(), contract.deployTransaction.hash) + blockNumber = await hre.ethers.provider.getBlockNumber() + console.log("BLOCK_NUMBER: %s", blockNumber) + err = await storage(path, L1SequencerImplStorageName, contract.address.toLocaleLowerCase(), blockNumber || 0) + if (err != '') { + return err + } + // return return '' } diff --git a/contracts/deploy/019-AdminTransfer.ts b/contracts/deploy/019-AdminTransfer.ts index 566396b90..a0cd1b58a 100644 --- a/contracts/deploy/019-AdminTransfer.ts +++ b/contracts/deploy/019-AdminTransfer.ts @@ -109,6 +109,7 @@ export const AdminTransferConcurrently = async ( ProxyStorageName.L1CrossDomainMessengerProxyStorageName, ProxyStorageName.L1MessageQueueWithGasPriceOracleProxyStorageName, ProxyStorageName.L1StakingProxyStorageName, + ProxyStorageName.L1SequencerProxyStorageName, // Added L1Sequencer ProxyStorageName.RollupProxyStorageName, ProxyStorageName.L1GatewayRouterProxyStorageName, ProxyStorageName.L1ETHGatewayProxyStorageName, @@ -159,6 +160,7 @@ export const AdminTransfer = async ( const RollupProxyStorageName = ProxyStorageName.RollupProxyStorageName const L1StakingProxyStorageName = ProxyStorageName.L1StakingProxyStorageName + const L1SequencerProxyStorageName = ProxyStorageName.L1SequencerProxyStorageName const L1GatewayRouterProxyStorageName = ProxyStorageName.L1GatewayRouterProxyStorageName const L1ETHGatewayProxyStorageName = ProxyStorageName.L1ETHGatewayProxyStorageName @@ -192,6 +194,13 @@ export const AdminTransfer = async ( return err } + // ************************ sequencer contracts admin change ************************ + // L1SequencerProxy admin change + err = await AdminTransferByProxyStorageName(hre, path, deployer, L1SequencerProxyStorageName) + if (err != '') { + return err + } + // ************************ rollup contracts admin change ************************ // RollupProxy admin change err = await AdminTransferByProxyStorageName(hre, path, deployer, RollupProxyStorageName) diff --git a/contracts/deploy/022-SequencerInit.ts b/contracts/deploy/022-SequencerInit.ts new file mode 100644 index 000000000..e8de25116 --- /dev/null +++ b/contracts/deploy/022-SequencerInit.ts @@ -0,0 +1,89 @@ +import "@nomiclabs/hardhat-web3"; +import "@nomiclabs/hardhat-ethers"; +import "@nomiclabs/hardhat-waffle"; + +import { + HardhatRuntimeEnvironment +} from 'hardhat/types'; +import { assertContractVariable, getContractAddressByName, awaitCondition } from "../src/deploy-utils"; +import { ethers } from 'ethers' + +import { + ImplStorageName, + ProxyStorageName, + ContractFactoryName, +} from "../src/types" + +export const SequencerInit = async ( + hre: HardhatRuntimeEnvironment, + path: string, + deployer: any, + configTmp: any +): Promise => { + // L1Sequencer addresses + const L1SequencerProxyAddress = getContractAddressByName(path, ProxyStorageName.L1SequencerProxyStorageName) + const L1SequencerImplAddress = getContractAddressByName(path, ImplStorageName.L1SequencerStorageName) + const L1SequencerFactory = await hre.ethers.getContractFactory(ContractFactoryName.L1Sequencer) + + const IL1SequencerProxy = await hre.ethers.getContractAt(ContractFactoryName.DefaultProxyInterface, L1SequencerProxyAddress, deployer) + + if ( + (await IL1SequencerProxy.implementation()).toLocaleLowerCase() !== L1SequencerImplAddress.toLocaleLowerCase() + ) { + console.log('Upgrading the L1Sequencer proxy...') + + // Owner is the deployer (will be transferred to multisig in production) + const owner = await deployer.getAddress() + + // Get initial sequencer address from config (first sequencer address) + // Note: l2SequencerAddresses is defined in contracts/src/deploy-config/l1.ts + const initialSequencer = (configTmp.l2SequencerAddresses && configTmp.l2SequencerAddresses.length > 0) + ? configTmp.l2SequencerAddresses[0] + : ethers.constants.AddressZero + + console.log('Initial sequencer address:', initialSequencer) + + // Upgrade and initialize the proxy with owner and initial sequencer + // Note: We set sequencer in initialize() to avoid TransparentUpgradeableProxy admin restriction + await IL1SequencerProxy.upgradeToAndCall( + L1SequencerImplAddress, + L1SequencerFactory.interface.encodeFunctionData('initialize', [owner, initialSequencer]) + ) + + await awaitCondition( + async () => { + return ( + (await IL1SequencerProxy.implementation()).toLocaleLowerCase() === L1SequencerImplAddress.toLocaleLowerCase() + ) + }, + 3000, + 1000 + ) + + const contractTmp = new ethers.Contract( + L1SequencerProxyAddress, + L1SequencerFactory.interface, + deployer, + ) + + await assertContractVariable( + contractTmp, + 'owner', + owner, + ) + + if (initialSequencer !== ethers.constants.AddressZero) { + await assertContractVariable( + contractTmp, + 'sequencer', + initialSequencer, + ) + console.log('L1SequencerProxy upgrade success, initial sequencer set:', initialSequencer) + } else { + console.log('L1SequencerProxy upgrade success (no initial sequencer set)') + } + } + return '' +} + +export default SequencerInit diff --git a/contracts/deploy/index.ts b/contracts/deploy/index.ts index 16f69f20b..a30ab90fb 100644 --- a/contracts/deploy/index.ts +++ b/contracts/deploy/index.ts @@ -10,6 +10,7 @@ import StakingInit from './018-StakingInit' import {AdminTransfer,AdminTransferByProxyStorageName, AdminTransferConcurrently} from './019-AdminTransfer' import ContractInit from './020-ContractInit' import StakingRegister from './021-StakingRegister' +import SequencerInit from './022-SequencerInit' export { @@ -28,5 +29,6 @@ export { AdminTransferByProxyStorageName, AdminTransferConcurrently, ContractInit, - StakingRegister + StakingRegister, + SequencerInit } \ No newline at end of file diff --git a/contracts/go.mod b/contracts/go.mod index c0cdc1473..514d63ee1 100644 --- a/contracts/go.mod +++ b/contracts/go.mod @@ -6,7 +6,7 @@ replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3. require ( github.com/iden3/go-iden3-crypto v0.0.16 - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 github.com/stretchr/testify v1.10.0 ) @@ -42,7 +42,7 @@ require ( github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect @@ -50,7 +50,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/tsdb v0.10.0 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/rs/cors v1.11.0 // indirect github.com/scroll-tech/zktrie v0.8.4 // indirect diff --git a/contracts/go.sum b/contracts/go.sum index 319f1f2b8..d0f44f830 100644 --- a/contracts/go.sum +++ b/contracts/go.sum @@ -125,8 +125,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -138,8 +138,8 @@ github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqky github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 h1:FUv9gtnvF+1AVrkoNGYbVOesi7E+STjdfD2mcqVaEY0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 h1:A8eygErKU6WKMipGWIemzwLeYkIGLd9yb/Ry3x+J9PQ= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -174,8 +174,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= diff --git a/contracts/src/types.ts b/contracts/src/types.ts index ea2577c89..7e518addd 100644 --- a/contracts/src/types.ts +++ b/contracts/src/types.ts @@ -18,6 +18,8 @@ const ContractFactoryName = { MultipleVersionRollupVerifier: 'MultipleVersionRollupVerifier', // staking L1Staking: 'L1Staking', + // L1 sequencer + L1Sequencer: 'L1Sequencer', // gateway L1GatewayRouter: 'L1GatewayRouter', L1StandardERC20Gateway: 'L1StandardERC20Gateway', @@ -40,6 +42,8 @@ const ProxyStorageName = { RollupProxyStorageName: 'Proxy__Rollup', // staking L1StakingProxyStorageName: 'Proxy__L1Staking', + // L1 sequencer + L1SequencerProxyStorageName: 'Proxy__L1Sequencer', // gateway L1GatewayRouterProxyStorageName: 'Proxy__L1GatewayRouter', L1StandardERC20GatewayProxyStorageName: 'Proxy__L1StandardERC20Gateway', @@ -71,6 +75,8 @@ const ImplStorageName = { MultipleVersionRollupVerifierStorageName: 'Impl__MultipleVersionRollupVerifier', // staking L1StakingStorageName: 'Impl__L1Staking', + // L1 sequencer + L1SequencerStorageName: 'Impl__L1Sequencer', // gateway L1GatewayRouterStorageName: 'Impl__L1GatewayRouter', L1StandardERC20GatewayStorageName: 'Impl__L1StandardERC20Gateway', diff --git a/contracts/tasks/deploy.ts b/contracts/tasks/deploy.ts index 9ee6739a9..a7ea6bb6a 100644 --- a/contracts/tasks/deploy.ts +++ b/contracts/tasks/deploy.ts @@ -21,6 +21,7 @@ import { AdminTransferConcurrently, ContractInit, StakingRegister, + SequencerInit, } from '../deploy/index' import { ethers } from "ethers"; @@ -120,6 +121,12 @@ task("initialize") console.log('Staking init failed, err: ', err) return } + console.log('\n---------------------------------- Sequencer init ----------------------------------') + err = await SequencerInit(hre, storagePath, deployer, config) + if (err != '') { + console.log('Sequencer init failed, err: ', err) + return + } console.log('\n---------------------------------- Admin Transfer ----------------------------------') if (concurrent === 'true') { err = await AdminTransferConcurrently(hre, storagePath, deployer, config) diff --git a/go-ethereum b/go-ethereum index 03910bc75..4f0f6e6bd 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 03910bc750a2301be4c1410b9f3c4d3741df251e +Subproject commit 4f0f6e6bd14178a19a6ef6e57bdcb48eb6612bf4 diff --git a/go.work b/go.work index d29dbaad9..d86ae4133 100644 --- a/go.work +++ b/go.work @@ -1,5 +1,7 @@ go 1.24.0 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.3-0.20260226075902-3692a2a2889c + use ( ./bindings ./contracts diff --git a/go.work.sum b/go.work.sum index 4403ab863..8c91e41ad 100644 --- a/go.work.sum +++ b/go.work.sum @@ -442,6 +442,8 @@ github.com/charithe/durationcheck v0.0.9 h1:mPP4ucLrf/rKZiIG/a9IPXHGlh8p4CzgpyTy github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= github.com/chavacava/garif v0.0.0-20220630083739-93517212f375 h1:E7LT642ysztPWE0dfz43cWOvMiF42DyTRC+eZIaO4yI= github.com/chavacava/garif v0.0.0-20220630083739-93517212f375/go.mod h1:4m1Rv7xfuwWPNKXlThldNuJvutYM6J95wNuuVmn55To= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= @@ -873,6 +875,8 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef h1:2jNeR4YUziVtswNP9sEFAI913cVrzH85T+8Q6LpYbT0= github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/karalabe/usb v0.0.2 h1:M6QQBNxF+CQ8OFvxrT90BA0qBOXymndZnk5q235mFc4= github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -981,6 +985,8 @@ github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLT github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0 h1:tEElEatulEHDeedTxwckzyYMA5c86fbmNIUL1hBIiTg= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= @@ -1007,8 +1013,8 @@ github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8q github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morph-l2/go-ethereum v1.10.14-0.20251125061742-69718a9dcab9/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 h1:FUv9gtnvF+1AVrkoNGYbVOesi7E+STjdfD2mcqVaEY0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206063816-522b70a5f16f h1:e8gfduHc4AKlR0fD6J3HXveP2Gp4PMvN2UfA9CYEvEc= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206063816-522b70a5f16f/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae h1:VeRdUYdCw49yizlSbMEn2SZ+gT+3IUKx8BqxyQdz+BY= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= @@ -1155,6 +1161,8 @@ github.com/sashamelentyev/usestdlibvars v1.13.0 h1:uObNudVEEHf6JbOJy5bgKJloA1bWj github.com/sashamelentyev/usestdlibvars v1.13.0/go.mod h1:D2Wb7niIYmTB+gB8z7kh8tyP5ccof1dQ+SFk+WW5NtY= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc= +github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/securego/gosec/v2 v2.13.1 h1:7mU32qn2dyC81MH9L2kefnQyRMUarfDER3iQyMHcjYM= diff --git a/node/blocktag/config.go b/node/blocktag/config.go index 6c392ce0f..43c282800 100644 --- a/node/blocktag/config.go +++ b/node/blocktag/config.go @@ -20,7 +20,6 @@ const ( // Config holds the configuration for BlockTagService type Config struct { - L1Addr string RollupAddress common.Address SafeConfirmations uint64 PollInterval time.Duration @@ -36,8 +35,6 @@ func DefaultConfig() *Config { // SetCliContext sets the configuration from CLI context func (c *Config) SetCliContext(ctx *cli.Context) error { - c.L1Addr = ctx.GlobalString(flags.L1NodeAddr.Name) - // Determine RollupAddress: use explicit flag, or mainnet default, or error if ctx.GlobalBool(flags.MainnetFlag.Name) { c.RollupAddress = node.MainnetRollupContractAddress diff --git a/node/blocktag/service.go b/node/blocktag/service.go index 86f9b8d70..45f7ecda3 100644 --- a/node/blocktag/service.go +++ b/node/blocktag/service.go @@ -63,22 +63,18 @@ type BlockTagService struct { // NewBlockTagService creates a new BlockTagService func NewBlockTagService( ctx context.Context, + l1Client *ethclient.Client, l2Client *types.RetryableClient, config *Config, logger tmlog.Logger, ) (*BlockTagService, error) { - if config.L1Addr == "" { - return nil, fmt.Errorf("L1 RPC address is required") + if l1Client == nil { + return nil, fmt.Errorf("L1 client is required") } if config.RollupAddress == (common.Address{}) { return nil, fmt.Errorf("Rollup contract address is required") } - l1Client, err := ethclient.Dial(config.L1Addr) - if err != nil { - return nil, fmt.Errorf("failed to connect to L1: %w", err) - } - rollup, err := bindings.NewRollup(config.RollupAddress, l1Client) if err != nil { return nil, fmt.Errorf("failed to create rollup binding: %w", err) @@ -122,7 +118,6 @@ func (s *BlockTagService) Stop() { s.logger.Info("Stopping BlockTagService") s.cancel() <-s.stop - s.l1Client.Close() s.logger.Info("BlockTagService stopped") } diff --git a/node/cmd/node/main.go b/node/cmd/node/main.go index 2a71f2a28..b056588c3 100644 --- a/node/cmd/node/main.go +++ b/node/cmd/node/main.go @@ -6,11 +6,17 @@ import ( "os" "os/signal" "path/filepath" + "strings" "syscall" + "time" + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/crypto" "github.com/morph-l2/go-ethereum/ethclient" + tmlog "github.com/tendermint/tendermint/libs/log" tmnode "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/upgrade" "github.com/urfave/cli" "morph-l2/bindings/bindings" @@ -20,6 +26,7 @@ import ( "morph-l2/node/db" "morph-l2/node/derivation" "morph-l2/node/flags" + "morph-l2/node/l1sequencer" "morph-l2/node/sequencer" "morph-l2/node/sequencer/mock" "morph-l2/node/sync" @@ -60,12 +67,20 @@ func L2NodeMain(ctx *cli.Context) error { tmNode *tmnode.Node dvNode *derivation.Derivation blockTagSvc *blocktag.BlockTagService + tracker *l1sequencer.L1Tracker + verifier *l1sequencer.SequencerVerifier + signer l1sequencer.Signer nodeConfig = node.DefaultConfig() ) isMockSequencer := ctx.GlobalBool(flags.MockEnabled.Name) isValidator := ctx.GlobalBool(flags.ValidatorEnable.Name) + // Apply consensus switch height if explicitly set via flag + if ctx.GlobalIsSet(flags.ConsensusSwitchHeight.Name) { + upgrade.SetUpgradeBlockHeight(ctx.GlobalInt64(flags.ConsensusSwitchHeight.Name)) + } + if err = nodeConfig.SetCliContext(ctx); err != nil { return err } @@ -118,14 +133,27 @@ func L2NodeMain(ctx *cli.Context) error { dvNode.Start() nodeConfig.Logger.Info("derivation node starting") } else { - // launch tendermint node + // ========== Create Syncer and L1 Sequencer Components ========== + syncer, err = node.NewSyncer(ctx, home, nodeConfig) + if err != nil { + return fmt.Errorf("failed to create syncer: %w", err) + } + + tracker, verifier, signer, err = initL1SequencerComponents(ctx, syncer.L1Client(), nodeConfig.Logger) + if err != nil { + return fmt.Errorf("failed to init L1 sequencer components: %w", err) + } + + // ========== Launch Tendermint Node ========== tmCfg, err := sequencer.LoadTmConfig(ctx, home) if err != nil { return err } tmVal := privval.LoadOrGenFilePV(tmCfg.PrivValidatorKeyFile(), tmCfg.PrivValidatorStateFile()) pubKey, _ := tmVal.GetPubKey() - newSyncerFunc := func() (*sync.Syncer, error) { return node.NewSyncer(ctx, home, nodeConfig) } + + // Create executor with syncer + newSyncerFunc := func() (*sync.Syncer, error) { return syncer, nil } // Reuse existing syncer executor, err = node.NewExecutor(newSyncerFunc, nodeConfig, pubKey) if err != nil { return err @@ -137,25 +165,19 @@ func L2NodeMain(ctx *cli.Context) error { } go ms.Start() } else { - if tmNode, err = sequencer.SetupNode(tmCfg, tmVal, executor, nodeConfig.Logger); err != nil { - return fmt.Errorf("failed to setup consensus node, error: %v", err) + tmNode, err = sequencer.SetupNode(tmCfg, tmVal, executor, nodeConfig.Logger, verifier, signer) + if err != nil { + return fmt.Errorf("failed to setup consensus node: %v", err) } if err = tmNode.Start(); err != nil { return fmt.Errorf("failed to start consensus node, error: %v", err) } } - // Start BlockTagService for sequencer mode - blockTagConfig := blocktag.DefaultConfig() - if err := blockTagConfig.SetCliContext(ctx); err != nil { - return fmt.Errorf("blocktag config set cli context error: %w", err) - } - blockTagSvc, err = blocktag.NewBlockTagService(context.Background(), executor.L2Client(), blockTagConfig, nodeConfig.Logger) + // ========== Initialize BlockTagService ========== + blockTagSvc, err = initBlockTagService(ctx, syncer.L1Client(), executor, nodeConfig.Logger) if err != nil { - return fmt.Errorf("failed to create BlockTagService: %w", err) - } - if err := blockTagSvc.Start(); err != nil { - return fmt.Errorf("failed to start BlockTagService: %w", err) + return fmt.Errorf("failed to init BlockTagService: %w", err) } } @@ -186,10 +208,102 @@ func L2NodeMain(ctx *cli.Context) error { if blockTagSvc != nil { blockTagSvc.Stop() } + if tracker != nil { + tracker.Stop() + } return nil } +// initL1SequencerComponents initializes all L1 sequencer related components: +// - L1Tracker: monitors L1 sync status +// - SequencerCache: caches L1 sequencer address (nil if contract not configured) +// - Signer: signs blocks (nil if private key not configured) +func initL1SequencerComponents( + ctx *cli.Context, + l1Client *ethclient.Client, + logger tmlog.Logger, +) (*l1sequencer.L1Tracker, *l1sequencer.SequencerVerifier, l1sequencer.Signer, error) { + if l1Client == nil { + return nil, nil, nil, fmt.Errorf("L1 client is required, check l1.rpc configuration") + } + + // Get config from flags + lagThreshold := ctx.GlobalDuration(flags.L1SyncLagThreshold.Name) + if lagThreshold == 0 { + lagThreshold = 5 * time.Minute // default + } + contractAddr := common.HexToAddress(ctx.GlobalString(flags.L1SequencerContractAddr.Name)) + seqPrivKeyHex := ctx.GlobalString(flags.SequencerPrivateKey.Name) + + // Initialize L1 Tracker + tracker := l1sequencer.NewL1Tracker(context.Background(), l1Client, lagThreshold, logger) + if err := tracker.Start(); err != nil { + return nil, nil, nil, fmt.Errorf("failed to start L1 tracker: %w", err) + } + logger.Info("L1 Tracker started", "lagThreshold", lagThreshold) + + // Initialize Sequencer Verifier (optional) + var verifier *l1sequencer.SequencerVerifier + if contractAddr != (common.Address{}) { + caller, err := bindings.NewL1SequencerCaller(contractAddr, l1Client) + if err != nil { + tracker.Stop() + return nil, nil, nil, fmt.Errorf("failed to create L1Sequencer caller: %w", err) + } + verifier = l1sequencer.NewSequencerVerifier(caller, logger) + logger.Info("Sequencer verifier initialized", "contract", contractAddr.Hex()) + } else { + logger.Info("L1 Sequencer contract not configured, verifier disabled") + } + + // Initialize Signer (optional) + var signer l1sequencer.Signer + if seqPrivKeyHex != "" { + seqPrivKeyHex = strings.TrimPrefix(seqPrivKeyHex, "0x") + privKey, err := crypto.HexToECDSA(seqPrivKeyHex) + if err != nil { + tracker.Stop() + return nil, nil, nil, fmt.Errorf("invalid sequencer private key: %w", err) + } + signer, err = l1sequencer.NewLocalSigner(privKey, verifier, logger) + if err != nil { + tracker.Stop() + return nil, nil, nil, err + } + logger.Info("Sequencer signer initialized", "address", signer.Address().Hex()) + } else { + logger.Info("Sequencer private key not configured, signer disabled") + } + + return tracker, verifier, signer, nil +} + +// initBlockTagService initializes the block tag service +func initBlockTagService( + ctx *cli.Context, + l1Client *ethclient.Client, + executor *node.Executor, + logger tmlog.Logger, +) (*blocktag.BlockTagService, error) { + config := blocktag.DefaultConfig() + if err := config.SetCliContext(ctx); err != nil { + return nil, err + } + + svc, err := blocktag.NewBlockTagService(context.Background(), l1Client, executor.L2Client(), config, logger) + if err != nil { + return nil, err + } + + if err := svc.Start(); err != nil { + return nil, err + } + + logger.Info("BlockTagService started") + return svc, nil +} + func homeDir(ctx *cli.Context) (string, error) { home := ctx.GlobalString(flags.Home.Name) if home == "" { diff --git a/node/core/batch.go b/node/core/batch.go index 987c4bf03..9c851956e 100644 --- a/node/core/batch.go +++ b/node/core/batch.go @@ -178,6 +178,17 @@ func (e *Executor) CalculateCapWithProposalBlock(currentBlockBytes []byte, curre return false, err } + // MPT fork: force batch points on the 1st and 2nd post-fork blocks, so the 1st post-fork block + // becomes a single-block batch: [H1, H2). + force, err := e.forceBatchPointForMPTFork(height, block.Timestamp, block.StateRoot, block.Hash) + if err != nil { + return false, err + } + if force { + e.logger.Info("MPT fork: force batch point", "height", height, "timestamp", block.Timestamp) + return true, nil + } + var exceeded bool if e.isBatchUpgraded(block.Timestamp) { exceeded, err = e.batchingCache.batchData.WillExceedCompressedSizeLimit(e.batchingCache.currentBlockContext, e.batchingCache.currentTxsPayload) @@ -187,6 +198,79 @@ func (e *Executor) CalculateCapWithProposalBlock(currentBlockBytes []byte, curre return exceeded, err } +// forceBatchPointForMPTFork forces batch points at the 1st and 2nd block after the MPT fork time. +// +// Design goals: +// - Minimal change: only affects batch-point decision logic. +// - Stability: CalculateCapWithProposalBlock can be called multiple times at the same height; return must be consistent. +// - Performance: after handling (or skipping beyond) the fork boundary, no more HeaderByNumber calls are made. +func (e *Executor) forceBatchPointForMPTFork(height uint64, blockTime uint64, stateRoot common.Hash, blockHash common.Hash) (bool, error) { + // If we already decided to force at this height, keep returning true without extra RPCs. + if e.mptForkForceHeight == height && height != 0 { + return true, nil + } + // If fork boundary is already handled and this isn't a forced height, fast exit. + if e.mptForkStage >= 2 { + return false, nil + } + + // Ensure we have fork time cached (0 means disabled). + if e.mptForkTime == 0 { + e.mptForkTime = e.l2Client.MPTForkTime() + } + forkTime := e.mptForkTime + if forkTime == 0 || blockTime < forkTime { + return false, nil + } + if height == 0 { + return false, nil + } + + // Check parent block time to detect the 1st post-fork block (H1). + parent, err := e.l2Client.HeaderByNumber(context.Background(), big.NewInt(int64(height-1))) + if err != nil { + return false, err + } + if parent.Time < forkTime { + // Log H1 (the 1st post-fork block) state root + // This stateRoot is intended to be used as the Rollup contract "genesis state root" + // when we reset/re-initialize the genesis state root during the MPT upgrade. + e.logger.Info( + "MPT_FORK_H1_GENESIS_STATE_ROOT", + "height", height, + "timestamp", blockTime, + "forkTime", forkTime, + "stateRoot", stateRoot.Hex(), + "blockHash", blockHash.Hex(), + ) + e.mptForkStage = 1 + e.mptForkForceHeight = height + return true, nil + } + + // If parent is already post-fork, we may be at the 2nd post-fork block (H2) or later. + if height < 2 { + // We cannot be H2; mark done to avoid future calls. + e.mptForkStage = 2 + return false, nil + } + + grandParent, err := e.l2Client.HeaderByNumber(context.Background(), big.NewInt(int64(height-2))) + if err != nil { + return false, err + } + if grandParent.Time < forkTime { + // This is H2 (2nd post-fork block). + e.mptForkStage = 2 + e.mptForkForceHeight = height + return true, nil + } + + // Beyond H2: nothing to do (can't retroactively fix). Mark done for performance. + e.mptForkStage = 2 + return false, nil +} + func (e *Executor) AppendBlsData(height int64, batchHash []byte, data l2node.BlsData) error { if len(batchHash) != 32 { return fmt.Errorf("wrong batchHash length. expected: 32, actual: %d", len(batchHash)) diff --git a/node/core/config.go b/node/core/config.go index a11b23897..9132f732c 100644 --- a/node/core/config.go +++ b/node/core/config.go @@ -32,6 +32,7 @@ var ( type Config struct { L2 *types.L2Config `json:"l2"` + L2Next *types.L2Config `json:"l2_next,omitempty"` // optional, for geth upgrade switch L2CrossDomainMessengerAddress common.Address `json:"cross_domain_messenger_address"` SequencerAddress common.Address `json:"sequencer_address"` GovAddress common.Address `json:"gov_address"` @@ -46,6 +47,7 @@ type Config struct { func DefaultConfig() *Config { return &Config{ L2: new(types.L2Config), + L2Next: nil, // optional, only for upgrade switch Logger: tmlog.NewTMLogger(tmlog.NewSyncWriter(os.Stdout)), MaxL1MessageNumPerBlock: 100, L2CrossDomainMessengerAddress: predeploys.L2CrossDomainMessengerAddr, @@ -126,6 +128,16 @@ func (c *Config) SetCliContext(ctx *cli.Context) error { c.L2.EngineAddr = l2EngineAddr c.L2.JwtSecret = secret + // L2Next is optional - only for upgrade switch (e.g., ZK to MPT) + l2NextEthAddr := ctx.GlobalString(flags.L2NextEthAddr.Name) + l2NextEngineAddr := ctx.GlobalString(flags.L2NextEngineAddr.Name) + if l2NextEthAddr != "" && l2NextEngineAddr != "" { + c.L2Next = &types.L2Config{ + EthAddr: l2NextEthAddr, + EngineAddr: l2NextEngineAddr, + JwtSecret: secret, // same secret + } + } if ctx.GlobalIsSet(flags.MaxL1MessageNumPerBlock.Name) { c.MaxL1MessageNumPerBlock = ctx.GlobalUint64(flags.MaxL1MessageNumPerBlock.Name) if c.MaxL1MessageNumPerBlock == 0 { @@ -161,6 +173,10 @@ func (c *Config) SetCliContext(ctx *cli.Context) error { c.DevSequencer = ctx.GlobalBool(flags.DevSequencer.Name) } + if ctx.GlobalIsSet(flags.BlsKeyCheckForkHeight.Name) { + c.BlsKeyCheckForkHeight = ctx.GlobalUint64(flags.BlsKeyCheckForkHeight.Name) + } + // setup batch upgrade index and fork heights switch { case ctx.GlobalIsSet(flags.MainnetFlag.Name): diff --git a/node/core/executor.go b/node/core/executor.go index a55c8c1bd..fe9ef5d86 100644 --- a/node/core/executor.go +++ b/node/core/executor.go @@ -56,6 +56,13 @@ type Executor struct { rollupABI *abi.ABI batchingCache *BatchingCache + // MPT fork handling: force batch points at the 1st and 2nd block after fork. + // This state machine exists to avoid repeated HeaderByNumber calls after the fork is handled, + // while keeping results stable if CalculateCapWithProposalBlock is called multiple times at the same height. + mptForkTime uint64 // cached from geth eth_config.morph.mptForkTime (0 means disabled/unknown) + mptForkStage uint8 // 0: not handled, 1: forced H1, 2: done (forced H2 or skipped beyond H2) + mptForkForceHeight uint64 // if equals current height, must return true (stability across multiple calls) + logger tmlog.Logger metrics *Metrics } @@ -71,6 +78,7 @@ func getNextL1MsgIndex(client *types.RetryableClient) (uint64, error) { func NewExecutor(newSyncFunc NewSyncerFunc, config *Config, tmPubKey crypto.PubKey) (*Executor, error) { logger := config.Logger logger = logger.With("module", "executor") + // L2 geth endpoint (required - current geth) aClient, err := authclient.DialContext(context.Background(), config.L2.EngineAddr, config.L2.JwtSecret) if err != nil { return nil, err @@ -80,7 +88,31 @@ func NewExecutor(newSyncFunc NewSyncerFunc, config *Config, tmPubKey crypto.PubK return nil, err } - l2Client := types.NewRetryableClient(aClient, eClient, config.Logger) + // L2Next endpoint (optional - for upgrade switch) + var aNextClient *authclient.Client + var eNextClient *ethclient.Client + if config.L2Next != nil && config.L2Next.EngineAddr != "" && config.L2Next.EthAddr != "" { + aNextClient, err = authclient.DialContext(context.Background(), config.L2Next.EngineAddr, config.L2Next.JwtSecret) + if err != nil { + return nil, err + } + eNextClient, err = ethclient.Dial(config.L2Next.EthAddr) + if err != nil { + return nil, err + } + logger.Info("L2Next geth configured (upgrade switch enabled)", "engineAddr", config.L2Next.EngineAddr, "ethAddr", config.L2Next.EthAddr) + } else { + logger.Info("L2Next geth not configured (no upgrade switch)") + } + + // Fetch geth config at startup (with retry to wait for geth) + gethCfg, err := types.FetchGethConfigWithRetry(config.L2.EthAddr, logger) + if err != nil { + return nil, fmt.Errorf("failed to fetch geth config: %w", err) + } + logger.Info("Geth config fetched", "switchTime", gethCfg.SwitchTime, "useZktrie", gethCfg.UseZktrie) + + l2Client := types.NewRetryableClient(aClient, eClient, aNextClient, eNextClient, gethCfg.SwitchTime, logger) index, err := getNextL1MsgIndex(l2Client) if err != nil { return nil, err @@ -123,6 +155,7 @@ func NewExecutor(newSyncFunc NewSyncerFunc, config *Config, tmPubKey crypto.PubK batchingCache: NewBatchingCache(), UpgradeBatchTime: config.UpgradeBatchTime, blsKeyCheckForkHeight: config.BlsKeyCheckForkHeight, + mptForkTime: l2Client.MPTForkTime(), logger: logger, metrics: PrometheusMetrics("morphnode"), } @@ -283,16 +316,39 @@ func (e *Executor) DeliverBlock(txs [][]byte, metaData []byte, consensusData l2n } if wrappedBlock.Number <= height { - e.logger.Info("ignore it, the block was delivered", "block number", wrappedBlock.Number) - if e.devSequencer { - return nil, consensusData.ValidatorSet, nil + e.logger.Info("block already delivered by geth (via P2P sync)", "block_number", wrappedBlock.Number) + // Even if block was already delivered (e.g., synced via P2P), we still need to check + // if MPT switch should happen, otherwise sentry nodes won't switch to the correct geth. + e.l2Client.EnsureSwitched(context.Background(), wrappedBlock.Timestamp, wrappedBlock.Number) + + // After switch, re-check height from the new geth client + // The block might exist in legacy geth but not in target geth after switch + newHeight, err := e.l2Client.BlockNumber(context.Background()) + if err != nil { + return nil, nil, err + } + if wrappedBlock.Number > newHeight { + e.logger.Info("block not in target geth after switch, need to deliver", + "block_number", wrappedBlock.Number, + "old_height", height, + "new_height", newHeight) + // Update height and continue to deliver the block + height = newHeight + } else { + if e.devSequencer { + return nil, consensusData.ValidatorSet, nil + } + return e.getParamsAndValsAtHeight(int64(wrappedBlock.Number)) } - return e.getParamsAndValsAtHeight(int64(wrappedBlock.Number)) } // We only accept the continuous blocks for now. // It acts like full sync. Snap sync is not enabled until the Geth enables snapshot with zkTrie if wrappedBlock.Number > height+1 { + e.logger.Error("!!! CRITICAL: Geth is behind - node BLOCKED !!!", + "consensus_block", wrappedBlock.Number, + "geth_height", height, + "action", "Switch to MPT-compatible geth IMMEDIATELY") return nil, nil, types.ErrWrongBlockNumber } @@ -324,7 +380,15 @@ func (e *Executor) DeliverBlock(txs [][]byte, metaData []byte, consensusData l2n } err = e.l2Client.NewL2Block(context.Background(), l2Block, batchHash) if err != nil { - e.logger.Error("failed to NewL2Block", "error", err) + e.logger.Error("========================================") + e.logger.Error("CRITICAL: Failed to deliver block to geth!") + e.logger.Error("========================================") + e.logger.Error("failed to NewL2Block", + "error", err, + "block_number", l2Block.Number, + "block_timestamp", l2Block.Timestamp) + e.logger.Error("HINT: If this occurs after MPT upgrade, your geth node may not support MPT blocks. " + + "Please ensure you are running an MPT-compatible geth node.") return nil, nil, err } diff --git a/node/derivation/config.go b/node/derivation/config.go index 439acbc42..efb1ceb31 100644 --- a/node/derivation/config.go +++ b/node/derivation/config.go @@ -34,6 +34,7 @@ const ( type Config struct { L1 *types.L1Config `json:"l1"` L2 *types.L2Config `json:"l2"` + L2Next *types.L2Config `json:"l2_next,omitempty"` // optional, for geth upgrade switch BeaconRpc string `json:"beacon_rpc"` RollupContractAddress common.Address `json:"rollup_contract_address"` StartHeight uint64 `json:"start_height"` @@ -55,6 +56,7 @@ func DefaultConfig() *Config { LogProgressInterval: DefaultLogProgressInterval, FetchBlockRange: DefaultFetchBlockRange, L2: new(types.L2Config), + L2Next: nil, // optional, only for upgrade switch } } @@ -135,6 +137,17 @@ func (c *Config) SetCliContext(ctx *cli.Context) error { c.L2.EthAddr = l2EthAddr c.L2.EngineAddr = l2EngineAddr c.L2.JwtSecret = secret + + // L2Next is optional - only for upgrade switch (e.g., ZK to MPT) + l2NextEthAddr := ctx.GlobalString(flags.L2NextEthAddr.Name) + l2NextEngineAddr := ctx.GlobalString(flags.L2NextEngineAddr.Name) + if l2NextEthAddr != "" && l2NextEngineAddr != "" { + c.L2Next = &types.L2Config{ + EthAddr: l2NextEthAddr, + EngineAddr: l2NextEngineAddr, + JwtSecret: secret, // same secret + } + } c.MetricsServerEnable = ctx.GlobalBool(flags.MetricsServerEnable.Name) c.MetricsHostname = ctx.GlobalString(flags.MetricsHostname.Name) c.MetricsPort = ctx.GlobalUint64(flags.MetricsPort.Name) diff --git a/node/derivation/derivation.go b/node/derivation/derivation.go index 5eccd2e9c..8fb311b0e 100644 --- a/node/derivation/derivation.go +++ b/node/derivation/derivation.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "math/big" - "os" "time" "github.com/morph-l2/go-ethereum" @@ -64,6 +63,10 @@ type Derivation struct { pollInterval time.Duration logProgressInterval time.Duration stop chan struct{} + + // geth upgrade config (fetched once at startup) + switchTime uint64 + useZktrie bool } type DeployContractBackend interface { @@ -78,6 +81,7 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, if err != nil { return nil, err } + // L2 geth endpoint (required - current geth) aClient, err := authclient.DialContext(context.Background(), cfg.L2.EngineAddr, cfg.L2.JwtSecret) if err != nil { return nil, err @@ -86,6 +90,24 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, if err != nil { return nil, err } + + // L2Next endpoint (optional - for upgrade switch) + var aNextClient *authclient.Client + var eNextClient *ethclient.Client + if cfg.L2Next != nil && cfg.L2Next.EngineAddr != "" && cfg.L2Next.EthAddr != "" { + aNextClient, err = authclient.DialContext(context.Background(), cfg.L2Next.EngineAddr, cfg.L2Next.JwtSecret) + if err != nil { + return nil, err + } + eNextClient, err = ethclient.Dial(cfg.L2Next.EthAddr) + if err != nil { + return nil, err + } + logger.Info("L2Next geth configured (upgrade switch enabled)", "engineAddr", cfg.L2Next.EngineAddr, "ethAddr", cfg.L2Next.EthAddr) + } else { + logger.Info("L2Next geth not configured (no upgrade switch)") + } + msgPasser, err := bindings.NewL2ToL1MessagePasser(predeploys.L2ToL1MessagePasserAddr, eClient) if err != nil { return nil, err @@ -116,6 +138,15 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, } baseHttp := NewBasicHTTPClient(cfg.BeaconRpc, logger) l1BeaconClient := NewL1BeaconClient(baseHttp) + + // Fetch geth config once at startup for root validation skip logic (with retry) + gethCfg, err := types.FetchGethConfigWithRetry(cfg.L2.EthAddr, logger) + if err != nil { + cancel() // cancel context to avoid leak + return nil, fmt.Errorf("failed to fetch geth config: %w", err) + } + logger.Info("Geth config fetched", "switchTime", gethCfg.SwitchTime, "useZktrie", gethCfg.UseZktrie) + return &Derivation{ ctx: ctx, db: db, @@ -129,7 +160,7 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, logger: logger, RollupContractAddress: cfg.RollupContractAddress, confirmations: cfg.L1.Confirmations, - l2Client: types.NewRetryableClient(aClient, eClient, tmlog.NewTMLogger(tmlog.NewSyncWriter(os.Stdout))), + l2Client: types.NewRetryableClient(aClient, eClient, aNextClient, eNextClient, gethCfg.SwitchTime, logger), cancel: cancel, stop: make(chan struct{}), startHeight: cfg.StartHeight, @@ -140,6 +171,8 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, metrics: metrics, l1BeaconClient: l1BeaconClient, L2ToL1MessagePasser: msgPasser, + switchTime: gethCfg.SwitchTime, + useZktrie: gethCfg.UseZktrie, }, nil } @@ -246,25 +279,47 @@ func (d *Derivation) derivationBlock(ctx context.Context) { d.logger.Error("get withdrawal root failed", "error", err) return } - if !bytes.Equal(lastHeader.Root.Bytes(), batchInfo.root.Bytes()) || !bytes.Equal(withdrawalRoot[:], batchInfo.withdrawalRoot.Bytes()) { - d.metrics.SetBatchStatus(stateException) - // TODO The challenge switch is currently on and will be turned on in the future - if d.validator != nil && d.validator.ChallengeEnable() { - if err := d.validator.ChallengeState(batchInfo.batchIndex); err != nil { - d.logger.Error("challenge state failed") - return + + rootMismatch := !bytes.Equal(lastHeader.Root.Bytes(), batchInfo.root.Bytes()) + withdrawalMismatch := !bytes.Equal(withdrawalRoot[:], batchInfo.withdrawalRoot.Bytes()) + + if rootMismatch || withdrawalMismatch { + // Check if should skip validation during upgrade transition + // Skip if: (before switch && MPT geth) or (after switch && ZK geth) + skipValidation := false + if d.switchTime > 0 { + beforeSwitch := lastHeader.Time < d.switchTime + if (beforeSwitch && !d.useZktrie) || (!beforeSwitch && d.useZktrie) { + skipValidation = true + d.logger.Info("Root validation skipped during upgrade transition", + "originStateRootHash", batchInfo.root, + "deriveStateRootHash", lastHeader.Root.Hex(), + "blockTimestamp", lastHeader.Time, + "switchTime", d.switchTime, + "useZktrie", d.useZktrie, + ) } } - d.logger.Info("root hash or withdrawal hash is not equal", - "originStateRootHash", batchInfo.root, - "deriveStateRootHash", lastHeader.Root.Hex(), - "batchWithdrawalRoot", batchInfo.withdrawalRoot.Hex(), - "deriveWithdrawalRoot", common.BytesToHash(withdrawalRoot[:]).Hex(), - ) - return - } else { - d.metrics.SetBatchStatus(stateNormal) + + if !skipValidation { + d.metrics.SetBatchStatus(stateException) + // TODO The challenge switch is currently on and will be turned on in the future + if d.validator != nil && d.validator.ChallengeEnable() { + if err := d.validator.ChallengeState(batchInfo.batchIndex); err != nil { + d.logger.Error("challenge state failed") + return + } + } + d.logger.Error("root hash or withdrawal hash is not equal", + "originStateRootHash", batchInfo.root, + "deriveStateRootHash", lastHeader.Root.Hex(), + "batchWithdrawalRoot", batchInfo.withdrawalRoot.Hex(), + "deriveWithdrawalRoot", common.BytesToHash(withdrawalRoot[:]).Hex(), + ) + return + } } + d.metrics.SetBatchStatus(stateNormal) d.metrics.SetL1SyncHeight(lg.BlockNumber) } @@ -468,6 +523,29 @@ func (d *Derivation) UnPackData(data []byte) (geth.RPCRollupBatch, error) { PostStateRoot: common.BytesToHash(rollupBatchData.PostStateRoot[:]), WithdrawRoot: common.BytesToHash(rollupBatchData.WithdrawalRoot[:]), } + } else if bytes.Equal(d.rollupABI.Methods["commitBatchWithProof"].ID, data[:4]) { + args, err := d.rollupABI.Methods["commitBatchWithProof"].Inputs.Unpack(data[4:]) + if err != nil { + return batch, fmt.Errorf("commitBatchWithProof Unpack error:%v", err) + } + rollupBatchData := args[0].(struct { + Version uint8 "json:\"version\"" + ParentBatchHeader []uint8 "json:\"parentBatchHeader\"" + LastBlockNumber uint64 "json:\"lastBlockNumber\"" + NumL1Messages uint16 "json:\"numL1Messages\"" + PrevStateRoot [32]uint8 "json:\"prevStateRoot\"" + PostStateRoot [32]uint8 "json:\"postStateRoot\"" + WithdrawalRoot [32]uint8 "json:\"withdrawalRoot\"" + }) + batch = geth.RPCRollupBatch{ + Version: uint(rollupBatchData.Version), + ParentBatchHeader: rollupBatchData.ParentBatchHeader, + LastBlockNumber: rollupBatchData.LastBlockNumber, + NumL1Messages: rollupBatchData.NumL1Messages, + PrevStateRoot: common.BytesToHash(rollupBatchData.PrevStateRoot[:]), + PostStateRoot: common.BytesToHash(rollupBatchData.PostStateRoot[:]), + WithdrawRoot: common.BytesToHash(rollupBatchData.WithdrawalRoot[:]), + } } else { return batch, types.ErrNotCommitBatchTx } diff --git a/node/flags/flags.go b/node/flags/flags.go index d344c62ce..3adc4adb9 100644 --- a/node/flags/flags.go +++ b/node/flags/flags.go @@ -1,6 +1,9 @@ package flags -import "github.com/urfave/cli" +import ( + "github.com/urfave/cli" + "time" +) const envVarPrefix = "MORPH_NODE_" @@ -28,6 +31,18 @@ var ( EnvVar: prefixEnvVar("L2_ENGINE_RPC"), } + L2NextEthAddr = cli.StringFlag{ + Name: "l2next.eth", + Usage: "Address of next L2 geth JSON-RPC endpoints to switch to (optional, for upgrades)", + EnvVar: prefixEnvVar("L2_NEXT_ETH_RPC"), + } + + L2NextEngineAddr = cli.StringFlag{ + Name: "l2next.engine", + Usage: "Address of next L2 geth Engine JSON-RPC endpoints to switch to (optional, for upgrades)", + EnvVar: prefixEnvVar("L2_NEXT_ENGINE_RPC"), + } + L2EngineJWTSecret = cli.StringFlag{ Name: "l2.jwt-secret", Usage: "Path to JWT secret key. Keys are 32 bytes, hex encoded in a file. A new key will be generated if left empty.", @@ -228,6 +243,27 @@ var ( Value: 10, } + // L1 Sequencer options + L1SequencerContractAddr = cli.StringFlag{ + Name: "l1.sequencerContract", + Usage: "L1 Sequencer contract address for signature verification", + EnvVar: prefixEnvVar("L1_SEQUENCER_CONTRACT"), + } + + L1SyncLagThreshold = cli.DurationFlag{ + Name: "l1.syncLagThreshold", + Usage: "L1 sync lag threshold for warning logs", + EnvVar: prefixEnvVar("L1_SYNC_LAG_THRESHOLD"), + Value: 5 * time.Minute, + } + + // Sequencer private key for block signing (hex encoded, without 0x prefix) + SequencerPrivateKey = cli.StringFlag{ + Name: "sequencer.privateKey", + Usage: "Sequencer private key for block signing (hex encoded)", + EnvVar: prefixEnvVar("SEQUENCER_PRIVATE_KEY"), + } + // Batch rules UpgradeBatchTime = cli.Uint64Flag{ Name: "upgrade.batchTime", @@ -239,6 +275,14 @@ var ( Usage: "Morph mainnet", } + // for test + ConsensusSwitchHeight = cli.Int64Flag{ + Name: "consensus.switchHeight", + Usage: "Block height at which the consensus switches to sequencer mode. Default -1 means upgrade disabled.", + EnvVar: prefixEnvVar("CONSENSUS_SWITCH_HEIGHT"), + Value: -1, + } + DerivationConfirmations = cli.Int64Flag{ Name: "derivation.confirmations", Usage: "The number of confirmations needed on L1 for finalization. If not set, the default value is l1.confirmations", @@ -298,6 +342,12 @@ var ( Value: 26660, EnvVar: prefixEnvVar("METRICS_PORT"), } + + BlsKeyCheckForkHeight = cli.Uint64Flag{ + Name: "bls-key-check-fork-height", + Usage: "The height at which the BLS key check fork occurs", + EnvVar: prefixEnvVar("BLS_KEY_CHECK_FORK_HEIGHT"), + } ) var Flags = []cli.Flag{ @@ -308,6 +358,8 @@ var Flags = []cli.Flag{ L2EthAddr, L2EngineAddr, L2EngineJWTSecret, + L2NextEthAddr, + L2NextEngineAddr, MaxL1MessageNumPerBlock, L2CrossDomainMessengerContractAddr, L2SequencerAddr, @@ -349,6 +401,14 @@ var Flags = []cli.Flag{ // blocktag options BlockTagSafeConfirmations, + // L1 Sequencer options + L1SequencerContractAddr, + L1SyncLagThreshold, + SequencerPrivateKey, + + // consensus + ConsensusSwitchHeight, + // batch rules UpgradeBatchTime, MainnetFlag, @@ -365,4 +425,6 @@ var Flags = []cli.Flag{ MetricsServerEnable, MetricsPort, MetricsHostname, + + BlsKeyCheckForkHeight, } diff --git a/node/go.mod b/node/go.mod index 5804512a2..a213f701d 100644 --- a/node/go.mod +++ b/node/go.mod @@ -2,7 +2,7 @@ module morph-l2/node go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.3 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.3-0.20260226075902-3692a2a2889c require ( github.com/cenkalti/backoff/v4 v4.1.3 @@ -11,7 +11,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/holiman/uint256 v1.2.4 github.com/klauspost/compress v1.17.9 - github.com/morph-l2/go-ethereum v1.10.14-0.20260115065009-43c9e2a6c033 + github.com/morph-l2/go-ethereum v1.10.14-0.20260122094524-88d3d86e4291 github.com/prometheus/client_golang v1.17.0 github.com/spf13/viper v1.13.0 github.com/stretchr/testify v1.10.0 @@ -75,7 +75,7 @@ require ( github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect github.com/minio/highwayhash v1.0.2 // indirect @@ -93,7 +93,7 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/tsdb v0.10.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/rs/cors v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/node/go.sum b/node/go.sum index 469923eff..c2cd2b8d4 100644 --- a/node/go.sum +++ b/node/go.sum @@ -339,8 +339,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= @@ -361,10 +361,10 @@ github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqky github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/morph-l2/go-ethereum v1.10.14-0.20260115065009-43c9e2a6c033 h1:p6Y/8mNezpH/UH0Tspa8iLM/GgjhILQFvdGsWDDlwdQ= -github.com/morph-l2/go-ethereum v1.10.14-0.20260115065009-43c9e2a6c033/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= -github.com/morph-l2/tendermint v0.3.3 h1:zsmzVJfKp+NuCr45ZUUY2ZJjnHAVLzwJLID6GxBR4i4= -github.com/morph-l2/tendermint v0.3.3/go.mod h1:TtCzp9l6Z6yDUiwv3TbqKqw8Q8RKp3fSz5+adO1/Y8w= +github.com/morph-l2/go-ethereum v1.10.14-0.20260122094524-88d3d86e4291 h1:NB82XxKvwcGjWd+qsiaDulh9Snv65sT+YRGXBMY/IXg= +github.com/morph-l2/go-ethereum v1.10.14-0.20260122094524-88d3d86e4291/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= +github.com/morph-l2/tendermint v0.3.3-0.20260226075902-3692a2a2889c h1:CzaQ/rK3nrqylN8JVr2htAsnu2xlg4u99SjzudzxrpM= +github.com/morph-l2/tendermint v0.3.3-0.20260226075902-3692a2a2889c/go.mod h1:TtCzp9l6Z6yDUiwv3TbqKqw8Q8RKp3fSz5+adO1/Y8w= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -429,8 +429,8 @@ github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSg github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= diff --git a/node/l1sequencer/signer.go b/node/l1sequencer/signer.go new file mode 100644 index 000000000..f03901ae3 --- /dev/null +++ b/node/l1sequencer/signer.go @@ -0,0 +1,71 @@ +package l1sequencer + +import ( + "context" + "crypto/ecdsa" + "fmt" + + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/crypto" + tmlog "github.com/tendermint/tendermint/libs/log" +) + +// Signer manages sequencer identity and signing capabilities. +// It abstracts the private key management, allowing for local key storage +// or remote signing services (e.g., HSM, KMS) in the future. +type Signer interface { + // Sign signs data with the sequencer's private key + Sign(data []byte) ([]byte, error) + + // Address returns the sequencer's address + Address() common.Address + + // IsActiveSequencer checks if this signer is the current L1 sequencer + IsActiveSequencer(ctx context.Context) (bool, error) +} + +// LocalSigner implements Signer with a local private key +type LocalSigner struct { + privKey *ecdsa.PrivateKey + address common.Address + verifier *SequencerVerifier + logger tmlog.Logger +} + +// NewLocalSigner creates a new LocalSigner with a local private key +func NewLocalSigner(privKey *ecdsa.PrivateKey, verifier *SequencerVerifier, logger tmlog.Logger) (*LocalSigner, error) { + if privKey == nil { + return nil, fmt.Errorf("private key is required") + } + + address := crypto.PubkeyToAddress(privKey.PublicKey) + + return &LocalSigner{ + privKey: privKey, + address: address, + verifier: verifier, + logger: logger.With("module", "signer"), + }, nil +} + +// Sign signs data with the sequencer's private key +func (s *LocalSigner) Sign(data []byte) ([]byte, error) { + signature, err := crypto.Sign(data, s.privKey) + if err != nil { + return nil, fmt.Errorf("failed to sign: %w", err) + } + return signature, nil +} + +// Address returns the sequencer's address +func (s *LocalSigner) Address() common.Address { + return s.address +} + +// IsActiveSequencer checks if this signer is the current L1 sequencer +func (s *LocalSigner) IsActiveSequencer(ctx context.Context) (bool, error) { + if s.verifier == nil { + return false, fmt.Errorf("sequencer verifier not set") + } + return s.verifier.IsSequencer(ctx, s.address) +} diff --git a/node/l1sequencer/tracker.go b/node/l1sequencer/tracker.go new file mode 100644 index 000000000..d8ea3b8c0 --- /dev/null +++ b/node/l1sequencer/tracker.go @@ -0,0 +1,86 @@ +package l1sequencer + +import ( + "context" + "time" + + "github.com/morph-l2/go-ethereum/ethclient" + tmlog "github.com/tendermint/tendermint/libs/log" +) + +// L1Tracker monitors L1 RPC sync status and logs warnings if behind. +// It runs as an independent service. +type L1Tracker struct { + ctx context.Context + cancel context.CancelFunc + l1Client *ethclient.Client + lagThreshold time.Duration + logger tmlog.Logger + stop chan struct{} +} + +// NewL1Tracker creates a new L1Tracker +func NewL1Tracker( + ctx context.Context, + l1Client *ethclient.Client, + lagThreshold time.Duration, + logger tmlog.Logger, +) *L1Tracker { + ctx, cancel := context.WithCancel(ctx) + return &L1Tracker{ + ctx: ctx, + cancel: cancel, + l1Client: l1Client, + lagThreshold: lagThreshold, + logger: logger.With("module", "l1tracker"), + stop: make(chan struct{}), + } +} + +// Start starts the L1Tracker +func (t *L1Tracker) Start() error { + t.logger.Info("Starting L1Tracker", "lagThreshold", t.lagThreshold) + go t.loop() + return nil +} + +// Stop stops the L1Tracker +func (t *L1Tracker) Stop() { + t.logger.Info("Stopping L1Tracker") + t.cancel() + <-t.stop +} + +func (t *L1Tracker) loop() { + defer close(t.stop) + + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-t.ctx.Done(): + return + case <-ticker.C: + t.checkL1SyncLag() + } + } +} + +func (t *L1Tracker) checkL1SyncLag() { + header, err := t.l1Client.HeaderByNumber(t.ctx, nil) + if err != nil { + t.logger.Error("Failed to get L1 header", "error", err) + return + } + + blockTime := time.Unix(int64(header.Time), 0) + lag := time.Since(blockTime) + if lag > t.lagThreshold { + t.logger.Error("L1 RPC is behind", + "latestBlock", header.Number.Uint64(), + "blockTime", blockTime.Format(time.RFC3339), + "lag", lag.Round(time.Second), + ) + } +} diff --git a/node/l1sequencer/verifier.go b/node/l1sequencer/verifier.go new file mode 100644 index 000000000..1cbf8517a --- /dev/null +++ b/node/l1sequencer/verifier.go @@ -0,0 +1,97 @@ +package l1sequencer + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/morph-l2/go-ethereum/accounts/abi/bind" + "github.com/morph-l2/go-ethereum/common" + tmlog "github.com/tendermint/tendermint/libs/log" + + "morph-l2/bindings/bindings" +) + +const ( + // CacheTTL is the time-to-live for the sequencer verifier cache + //CacheTTL = 30 * time.Minute + CacheTTL = 10 * time.Second +) + +// SequencerVerifier verifies L1 sequencer status with caching. +// It provides IsSequencer() for checking if an address is the current sequencer. +type SequencerVerifier struct { + mutex sync.Mutex + sequencer common.Address + cacheExpiry time.Time + + caller *bindings.L1SequencerCaller + logger tmlog.Logger +} + +// NewSequencerVerifier creates a new SequencerVerifier +func NewSequencerVerifier(caller *bindings.L1SequencerCaller, logger tmlog.Logger) *SequencerVerifier { + return &SequencerVerifier{ + caller: caller, + logger: logger.With("module", "l1sequencer_verifier"), + } +} + +// flushCache refreshes the cache (caller must hold the lock) +func (c *SequencerVerifier) flushCache(ctx context.Context) error { + newSeq, err := c.caller.GetSequencer(&bind.CallOpts{Context: ctx}) + if err != nil { + return fmt.Errorf("failed to get sequencer from L1: %w", err) + } + + if c.sequencer != newSeq { + c.logger.Info("Sequencer address updated", + "old", c.sequencer.Hex(), + "new", newSeq.Hex()) + } + + c.sequencer = newSeq + c.cacheExpiry = time.Now().Add(CacheTTL) + return nil +} + +// IsSequencer checks if the given address is the current sequencer. +// It uses lazy loading: refreshes cache if expired, and retries on miss. +func (c *SequencerVerifier) IsSequencer(ctx context.Context, addr common.Address) (bool, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + // Cache expired, refresh + if time.Now().After(c.cacheExpiry) { + if err := c.flushCache(ctx); err != nil { + return false, err + } + } + + // Cache hit + if c.sequencer == addr { + return true, nil + } + + // Cache miss - maybe sequencer just updated, force refresh once + if err := c.flushCache(ctx); err != nil { + return false, err + } + + return c.sequencer == addr, nil +} + +// GetSequencer returns the cached sequencer address (refreshes if expired) +func (c *SequencerVerifier) GetSequencer(ctx context.Context) (common.Address, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + if time.Now().After(c.cacheExpiry) { + if err := c.flushCache(ctx); err != nil { + return common.Address{}, err + } + } + + return c.sequencer, nil +} diff --git a/node/sequencer/tm_node.go b/node/sequencer/tm_node.go index 6f6bf994c..b9cbffe38 100644 --- a/node/sequencer/tm_node.go +++ b/node/sequencer/tm_node.go @@ -14,11 +14,13 @@ import ( tmnode "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/proxy" + tmsequencer "github.com/tendermint/tendermint/sequencer" "github.com/tendermint/tendermint/types" "github.com/urfave/cli" node "morph-l2/node/core" "morph-l2/node/flags" + "morph-l2/node/l1sequencer" nodetypes "morph-l2/node/types" ) @@ -51,7 +53,17 @@ func LoadTmConfig(ctx *cli.Context, home string) (*config.Config, error) { return tmCfg, nil } -func SetupNode(tmCfg *config.Config, privValidator types.PrivValidator, executor *node.Executor, logger tmlog.Logger) (*tmnode.Node, error) { +// SetupNode creates a tendermint node with the given configuration. +// verifier: L1 sequencer verifier for signature verification (optional, can be nil) +// signer: sequencer signer for block signing (optional, can be nil) +func SetupNode( + tmCfg *config.Config, + privValidator types.PrivValidator, + executor *node.Executor, + logger tmlog.Logger, + verifier *l1sequencer.SequencerVerifier, + signer l1sequencer.Signer, +) (*tmnode.Node, error) { nodeLogger := logger.With("module", "main") nodeKey, err := p2p.LoadOrGenNodeKey(tmCfg.NodeKeyFile()) @@ -67,7 +79,12 @@ func SetupNode(tmCfg *config.Config, privValidator types.PrivValidator, executor return nil, fmt.Errorf("failed to load bls priv key") } - //var app types.Application + // Build verifier (SequencerVerifier implements tmsequencer.SequencerVerifier interface) + var tmVerifier tmsequencer.SequencerVerifier + if verifier != nil { + tmVerifier = verifier + } + n, err := tmnode.NewNode( tmCfg, executor, @@ -79,6 +96,8 @@ func SetupNode(tmCfg *config.Config, privValidator types.PrivValidator, executor tmnode.DefaultDBProvider, tmnode.DefaultMetricsProvider(tmCfg.Instrumentation), nodeLogger, + tmVerifier, + signer, ) return n, err } diff --git a/node/sync/bridge_client.go b/node/sync/bridge_client.go index bab51c45e..e8a73abf3 100644 --- a/node/sync/bridge_client.go +++ b/node/sync/bridge_client.go @@ -24,6 +24,11 @@ type BridgeClient struct { logger tmlog.Logger } +// L1Client returns the underlying L1 client (for sharing with other services) +func (c *BridgeClient) L1Client() *ethclient.Client { + return c.l1Client +} + func NewBridgeClient(l1Client *ethclient.Client, l1MessageQueueAddress common.Address, confirmations rpc.BlockNumber, logger tmlog.Logger) (*BridgeClient, error) { logger = logger.With("module", "bridge") filter, err := bindings.NewL1MessageQueueWithGasPriceOracleFilterer(l1MessageQueueAddress, l1Client) diff --git a/node/sync/syncer.go b/node/sync/syncer.go index 9c109ac06..c9948983a 100644 --- a/node/sync/syncer.go +++ b/node/sync/syncer.go @@ -196,3 +196,11 @@ func (s *Syncer) ReadL1MessagesInRange(start, end uint64) []types.L1Message { func (s *Syncer) LatestSynced() uint64 { return s.latestSynced } + +// L1Client returns the underlying L1 client (for sharing with other services) +func (s *Syncer) L1Client() *ethclient.Client { + if s.bridgeClient != nil { + return s.bridgeClient.L1Client() + } + return nil +} diff --git a/node/types/retryable_client.go b/node/types/retryable_client.go index 2af9f6d3f..10107d4d6 100644 --- a/node/types/retryable_client.go +++ b/node/types/retryable_client.go @@ -2,8 +2,12 @@ package types import ( "context" + "encoding/json" + "fmt" "math/big" "strings" + "sync/atomic" + "time" "github.com/cenkalti/backoff/v4" "github.com/morph-l2/go-ethereum" @@ -12,6 +16,7 @@ import ( "github.com/morph-l2/go-ethereum/eth/catalyst" "github.com/morph-l2/go-ethereum/ethclient" "github.com/morph-l2/go-ethereum/ethclient/authclient" + "github.com/morph-l2/go-ethereum/rpc" tmlog "github.com/tendermint/tendermint/libs/log" ) @@ -24,30 +29,275 @@ const ( ExecutionAborted = "execution aborted" Timeout = "timed out" DiscontinuousBlockError = "discontinuous block number" + + // Geth connection retry settings + GethRetryAttempts = 60 // max retry attempts + GethRetryInterval = 5 * time.Second // interval between retries ) +// configResponse represents the eth_config RPC response (EIP-7910) +type configResponse struct { + Current *forkConfig `json:"current"` + Next *forkConfig `json:"next"` + Last *forkConfig `json:"last"` +} + +// forkConfig represents a single fork configuration +type forkConfig struct { + ActivationTime uint64 `json:"activationTime"` + ChainId string `json:"chainId"` + ForkId string `json:"forkId"` + Precompiles map[string]string `json:"precompiles"` + SystemContracts map[string]string `json:"systemContracts"` + Morph *morphExtension `json:"morph,omitempty"` +} + +// morphExtension contains Morph-specific configuration fields +type morphExtension struct { + UseZktrie bool `json:"useZktrie"` + MPTForkTime *uint64 `json:"mptForkTime,omitempty"` +} + +// GethConfig holds the configuration fetched from geth via eth_config API +type GethConfig struct { + SwitchTime uint64 + UseZktrie bool +} + +// FetchGethConfigWithRetry fetches geth config with retry, waiting for geth to be ready. +func FetchGethConfigWithRetry(rpcURL string, logger tmlog.Logger) (*GethConfig, error) { + var lastErr error + for i := 0; i < GethRetryAttempts; i++ { + config, err := FetchGethConfig(rpcURL, logger) + if err == nil { + return config, nil + } + lastErr = err + logger.Info("Waiting for geth to be ready...", "attempt", i+1, "error", err) + time.Sleep(GethRetryInterval) + } + return nil, fmt.Errorf("geth not ready after %d attempts: %w", GethRetryAttempts, lastErr) +} + +// FetchGethConfig fetches the geth configuration via eth_config API +func FetchGethConfig(rpcURL string, logger tmlog.Logger) (*GethConfig, error) { + client, err := rpc.Dial(rpcURL) + if err != nil { + return nil, fmt.Errorf("failed to connect to geth: %w", err) + } + defer client.Close() + + var result json.RawMessage + if err := client.Call(&result, "eth_config"); err != nil { + return nil, fmt.Errorf("eth_config call failed: %w", err) + } + + var resp configResponse + if err := json.Unmarshal(result, &resp); err != nil { + return nil, fmt.Errorf("failed to parse eth_config response: %w", err) + } + + config := &GethConfig{} + + // Get useZktrie from current config + if resp.Current != nil && resp.Current.Morph != nil { + config.UseZktrie = resp.Current.Morph.UseZktrie + logger.Info("Fetched useZktrie from geth", "useZktrie", config.UseZktrie) + } + + // Try to get mptForkTime from current config + if resp.Current != nil && resp.Current.Morph != nil && resp.Current.Morph.MPTForkTime != nil { + config.SwitchTime = *resp.Current.Morph.MPTForkTime + logger.Info("Fetched MPT fork time from geth", "mptForkTime", config.SwitchTime, "source", "current") + return config, nil + } + + // Fallback to next config + if resp.Next != nil && resp.Next.Morph != nil && resp.Next.Morph.MPTForkTime != nil { + config.SwitchTime = *resp.Next.Morph.MPTForkTime + logger.Info("Fetched MPT fork time from geth", "mptForkTime", config.SwitchTime, "source", "next") + return config, nil + } + + // Fallback to last config + if resp.Last != nil && resp.Last.Morph != nil && resp.Last.Morph.MPTForkTime != nil { + config.SwitchTime = *resp.Last.Morph.MPTForkTime + logger.Info("Fetched MPT fork time from geth", "mptForkTime", config.SwitchTime, "source", "last") + return config, nil + } + + logger.Info("MPT fork time not configured in geth, switch disabled") + return config, nil +} + type RetryableClient struct { - authClient *authclient.Client - ethClient *ethclient.Client - b backoff.BackOff - logger tmlog.Logger + authClient *authclient.Client // current geth + ethClient *ethclient.Client // current geth + nextAuthClient *authclient.Client // next geth (for upgrade switch) + nextEthClient *ethclient.Client // next geth (for upgrade switch) + switchTime uint64 // timestamp to switch to next geth + switched atomic.Bool // whether switched to next geth + b backoff.BackOff + logger tmlog.Logger } -// NewRetryableClient make the client retryable -// Will retry calling the api, if the connection is refused -func NewRetryableClient(authClient *authclient.Client, ethClient *ethclient.Client, logger tmlog.Logger) *RetryableClient { +// MPTForkTime returns the configured MPT fork/switch timestamp fetched from geth (eth_config). +// Note: this is a local value stored in the client; it does not perform any RPC. +func (rc *RetryableClient) MPTForkTime() uint64 { + return rc.switchTime +} + +// NewRetryableClient creates a new retryable client with the given switch time. +// Will retry calling the api, if the connection is refused. +// +// If nextAuthClient or nextEthClient is nil, switch is disabled and only current client is used. +// This is useful for nodes that don't need to switch geth (most nodes). +// +// The switchTime should be fetched via FetchGethConfig before calling this function. +func NewRetryableClient(authClient *authclient.Client, ethClient *ethclient.Client, nextAuthClient *authclient.Client, nextEthClient *ethclient.Client, switchTime uint64, logger tmlog.Logger) *RetryableClient { logger = logger.With("module", "retryClient") - return &RetryableClient{ - authClient: authClient, - ethClient: ethClient, - b: backoff.NewExponentialBackOff(), - logger: logger, + + // If next client is not configured, disable switch + if nextAuthClient == nil || nextEthClient == nil { + logger.Info("L2Next client not configured, switch disabled") + return &RetryableClient{ + authClient: authClient, + ethClient: ethClient, + nextAuthClient: authClient, // fallback to current + nextEthClient: ethClient, // fallback to current + switchTime: switchTime, + b: backoff.NewExponentialBackOff(), + logger: logger, + } + } + // Check if switch time has already passed at startup + now := uint64(time.Now().Unix()) + alreadySwitched := switchTime > 0 && now >= switchTime + + if alreadySwitched { + logger.Info("Switch time already passed at startup, starting with next client", + "switchTime", switchTime, + "currentTime", now) + } else { + logger.Info("Geth switch enabled", "switchTime", switchTime) + } + + rc := &RetryableClient{ + authClient: authClient, + ethClient: ethClient, + nextAuthClient: nextAuthClient, + nextEthClient: nextEthClient, + switchTime: switchTime, + b: backoff.NewExponentialBackOff(), + logger: logger, + } + + // If switch time already passed, mark as switched immediately + if alreadySwitched { + rc.switched.Store(true) + } + + return rc +} + +func (rc *RetryableClient) aClient() *authclient.Client { + if !rc.switched.Load() { + return rc.authClient + } + return rc.nextAuthClient +} + +func (rc *RetryableClient) eClient() *ethclient.Client { + if !rc.switched.Load() { + return rc.ethClient + } + return rc.nextEthClient +} + +// EnsureSwitched checks if switch time has been reached and switches to next client if needed. +// This should be called when the block is already delivered (e.g., synced via P2P) to ensure +// the client switch happens even if NewL2Block is not called. +func (rc *RetryableClient) EnsureSwitched(ctx context.Context, timeStamp uint64, number uint64) { + rc.switchClient(ctx, timeStamp, number) +} + +func (rc *RetryableClient) switchClient(ctx context.Context, timeStamp uint64, number uint64) { + if rc.switched.Load() { + return + } + if rc.switchTime == 0 { + return + } + if timeStamp < rc.switchTime { + return + } + + rc.logger.Info("========================================") + rc.logger.Info("GETH UPGRADE: Switch time reached!") + rc.logger.Info("========================================") + rc.logger.Info("Switch time reached, switching from current client to next client", + "switch_time", rc.switchTime, + "current_time", timeStamp, + "target_block", number) + rc.logger.Info("Current status: connected to current geth, waiting for next geth to sync...") + + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + startTime := time.Now() + lastLogTime := startTime + + for { + remote, err := rc.nextEthClient.BlockNumber(ctx) + if err != nil { + rc.logger.Error("Failed to get next geth block number", + "error", err, + "hint", "Please ensure next geth is running and accessible") + <-ticker.C + continue + } + + if remote+1 >= number { + // Get next geth's latest block hash for debugging + targetHeader, headerErr := rc.nextEthClient.HeaderByNumber(ctx, big.NewInt(int64(remote))) + targetBlockHash := "unknown" + targetStateRoot := "unknown" + if headerErr == nil && targetHeader != nil { + targetBlockHash = targetHeader.Hash().Hex() + targetStateRoot = targetHeader.Root.Hex() + } + + rc.switched.Store(true) + rc.logger.Info("========================================") + rc.logger.Info("GETH UPGRADE: Successfully switched!") + rc.logger.Info("========================================") + rc.logger.Info("Successfully switched to next client", + "remote_block", remote, + "target_block", number, + "target_block_hash", targetBlockHash, + "target_state_root", targetStateRoot, + "wait_duration", time.Since(startTime)) + return + } + + if time.Since(lastLogTime) >= 5*time.Second { + rc.logger.Error("!!! WAITING: Node BLOCKED waiting for next geth !!!", + "next_geth_block", remote, + "target_block", number, + "blocks_behind", number-remote-1, + "wait_duration", time.Since(startTime)) + lastLogTime = time.Now() + } + + <-ticker.C } } func (rc *RetryableClient) AssembleL2Block(ctx context.Context, number *big.Int, transactions eth.Transactions) (ret *catalyst.ExecutableL2Data, err error) { + timestamp := uint64(time.Now().Unix()) if retryErr := backoff.Retry(func() error { - resp, respErr := rc.authClient.AssembleL2Block(ctx, number, transactions) + rc.switchClient(ctx, timestamp, number.Uint64()) + resp, respErr := rc.aClient().AssembleL2Block(ctx, ×tamp, number, transactions) if respErr != nil { rc.logger.Info("failed to AssembleL2Block", "error", respErr) if retryableError(respErr) { @@ -64,8 +314,9 @@ func (rc *RetryableClient) AssembleL2Block(ctx context.Context, number *big.Int, } func (rc *RetryableClient) ValidateL2Block(ctx context.Context, executableL2Data *catalyst.ExecutableL2Data) (ret bool, err error) { + rc.switchClient(ctx, executableL2Data.Timestamp, executableL2Data.Number) if retryErr := backoff.Retry(func() error { - resp, respErr := rc.authClient.ValidateL2Block(ctx, executableL2Data) + resp, respErr := rc.aClient().ValidateL2Block(ctx, executableL2Data) if respErr != nil { rc.logger.Info("failed to ValidateL2Block", "error", respErr) if retryableError(respErr) { @@ -82,10 +333,14 @@ func (rc *RetryableClient) ValidateL2Block(ctx context.Context, executableL2Data } func (rc *RetryableClient) NewL2Block(ctx context.Context, executableL2Data *catalyst.ExecutableL2Data, batchHash *common.Hash) (err error) { + rc.switchClient(ctx, executableL2Data.Timestamp, executableL2Data.Number) + if retryErr := backoff.Retry(func() error { - respErr := rc.authClient.NewL2Block(ctx, executableL2Data, batchHash) + respErr := rc.aClient().NewL2Block(ctx, executableL2Data, batchHash) if respErr != nil { - rc.logger.Info("failed to NewL2Block", "error", respErr) + rc.logger.Error("NewL2Block failed", + "block_number", executableL2Data.Number, + "error", respErr) if retryableError(respErr) { return respErr } @@ -99,8 +354,9 @@ func (rc *RetryableClient) NewL2Block(ctx context.Context, executableL2Data *cat } func (rc *RetryableClient) NewSafeL2Block(ctx context.Context, safeL2Data *catalyst.SafeL2Data) (ret *eth.Header, err error) { + rc.switchClient(ctx, safeL2Data.Timestamp, safeL2Data.Number) if retryErr := backoff.Retry(func() error { - resp, respErr := rc.authClient.NewSafeL2Block(ctx, safeL2Data) + resp, respErr := rc.aClient().NewSafeL2Block(ctx, safeL2Data) if respErr != nil { rc.logger.Info("failed to NewSafeL2Block", "error", respErr) if retryableError(respErr) { @@ -118,7 +374,7 @@ func (rc *RetryableClient) NewSafeL2Block(ctx context.Context, safeL2Data *catal func (rc *RetryableClient) CommitBatch(ctx context.Context, batch *eth.RollupBatch, signatures []eth.BatchSignature) (err error) { if retryErr := backoff.Retry(func() error { - respErr := rc.authClient.CommitBatch(ctx, batch, signatures) + respErr := rc.aClient().CommitBatch(ctx, batch, signatures) if respErr != nil { rc.logger.Info("failed to CommitBatch", "error", respErr) if retryableError(respErr) { @@ -135,7 +391,7 @@ func (rc *RetryableClient) CommitBatch(ctx context.Context, batch *eth.RollupBat func (rc *RetryableClient) AppendBlsSignature(ctx context.Context, batchHash common.Hash, signature eth.BatchSignature) (err error) { if retryErr := backoff.Retry(func() error { - respErr := rc.authClient.AppendBlsSignature(ctx, batchHash, signature) + respErr := rc.aClient().AppendBlsSignature(ctx, batchHash, signature) if respErr != nil { rc.logger.Info("failed to call AppendBlsSignature", "error", respErr) if retryableError(respErr) { @@ -152,7 +408,7 @@ func (rc *RetryableClient) AppendBlsSignature(ctx context.Context, batchHash com func (rc *RetryableClient) BlockNumber(ctx context.Context) (ret uint64, err error) { if retryErr := backoff.Retry(func() error { - resp, respErr := rc.ethClient.BlockNumber(ctx) + resp, respErr := rc.eClient().BlockNumber(ctx) if respErr != nil { rc.logger.Info("failed to call BlockNumber", "error", respErr) if retryableError(respErr) { @@ -170,7 +426,7 @@ func (rc *RetryableClient) BlockNumber(ctx context.Context) (ret uint64, err err func (rc *RetryableClient) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (ret *eth.Header, err error) { if retryErr := backoff.Retry(func() error { - resp, respErr := rc.ethClient.HeaderByNumber(ctx, blockNumber) + resp, respErr := rc.eClient().HeaderByNumber(ctx, blockNumber) if respErr != nil { rc.logger.Info("failed to call HeaderByNumber", "error", respErr) if retryableError(respErr) { @@ -206,7 +462,7 @@ func (rc *RetryableClient) BlockByNumber(ctx context.Context, blockNumber *big.I func (rc *RetryableClient) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) (ret []byte, err error) { if retryErr := backoff.Retry(func() error { - resp, respErr := rc.ethClient.CallContract(ctx, call, blockNumber) + resp, respErr := rc.eClient().CallContract(ctx, call, blockNumber) if respErr != nil { rc.logger.Info("failed to call eth_call", "error", respErr) if retryableError(respErr) { @@ -224,7 +480,7 @@ func (rc *RetryableClient) CallContract(ctx context.Context, call ethereum.CallM func (rc *RetryableClient) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) (ret []byte, err error) { if retryErr := backoff.Retry(func() error { - resp, respErr := rc.ethClient.CodeAt(ctx, contract, blockNumber) + resp, respErr := rc.eClient().CodeAt(ctx, contract, blockNumber) if respErr != nil { rc.logger.Info("failed to call eth_getCode", "error", respErr) if retryableError(respErr) { diff --git a/ops/devnet-morph/devnet/__init__.py b/ops/devnet-morph/devnet/__init__.py index 4ec81c897..385a7a2a3 100644 --- a/ops/devnet-morph/devnet/__init__.py +++ b/ops/devnet-morph/devnet/__init__.py @@ -248,6 +248,7 @@ def devnet_deploy(paths, args): env_data['MORPH_ROLLUP'] = addresses['Proxy__Rollup'] env_data['RUST_LOG'] = rust_log_level env_data['Proxy__L1Staking'] = addresses['Proxy__L1Staking'] + env_data['L1_SEQUENCER_CONTRACT'] = addresses.get('Proxy__L1Sequencer', '') envfile.seek(0) for key, value in env_data.items(): envfile.write(f'{key}={value}\n') diff --git a/ops/docker-sequencer-test/docker-compose.override.yml b/ops/docker-sequencer-test/docker-compose.override.yml index 50d960e27..9cc69cae8 100644 --- a/ops/docker-sequencer-test/docker-compose.override.yml +++ b/ops/docker-sequencer-test/docker-compose.override.yml @@ -15,8 +15,8 @@ services: morph-geth-2: image: morph-geth-test:latest - # morph-geth-3: - # image: morph-geth-test:latest + morph-geth-3: + image: morph-geth-test:latest node-0: image: morph-node-test:latest @@ -24,20 +24,42 @@ services: context: ../.. dockerfile: ops/docker-sequencer-test/Dockerfile.l2-node-test environment: - - SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80} + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 + - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} + - MORPH_NODE_CONSENSUS_SWITCH_HEIGHT=${CONSENSUS_SWITCH_HEIGHT:-10} + node-1: image: morph-node-test:latest + environment: + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0x0890c388c3bf5e04fee1d8f3c117e5f44f435ced7baf7bfd66c10e1f3a3f4b10 + - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} + - MORPH_NODE_CONSENSUS_SWITCH_HEIGHT=${CONSENSUS_SWITCH_HEIGHT:-10} + node-2: image: morph-node-test:latest + environment: + - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} + - MORPH_NODE_CONSENSUS_SWITCH_HEIGHT=${CONSENSUS_SWITCH_HEIGHT:-10} + + + node-3: + image: morph-node-test:latest + environment: + - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} + - MORPH_NODE_CONSENSUS_SWITCH_HEIGHT=${CONSENSUS_SWITCH_HEIGHT:-10} - # node-3: - # image: morph-node-test:latest sentry-geth-0: image: morph-geth-test:latest sentry-node-0: image: morph-node-test:latest + environment: + - MORPH_NODE_CONSENSUS_SWITCH_HEIGHT=${CONSENSUS_SWITCH_HEIGHT:-10} diff --git a/ops/docker-sequencer-test/run-test.sh b/ops/docker-sequencer-test/run-test.sh index 823ba5273..81361fefa 100755 --- a/ops/docker-sequencer-test/run-test.sh +++ b/ops/docker-sequencer-test/run-test.sh @@ -24,7 +24,7 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } # Configuration -UPGRADE_HEIGHT=${UPGRADE_HEIGHT:-50} +UPGRADE_HEIGHT=${UPGRADE_HEIGHT:-10} L2_RPC="http://127.0.0.1:8545" L2_RPC_NODE1="http://127.0.0.1:8645" @@ -77,18 +77,13 @@ wait_for_block() { # ========== Setup Functions ========== -# Update upgrade height in tendermint code +# Export consensus switch height as environment variable for Docker containers +# The morphnode binary reads MORPH_NODE_CONSENSUS_SWITCH_HEIGHT at runtime set_upgrade_height() { local height=$1 - log_info "Setting upgrade height to $height..." - - local upgrade_file="$MORPH_ROOT/../tendermint/upgrade/upgrade.go" - if [ -f "$upgrade_file" ]; then - sed -i '' "s/UpgradeBlockHeight int64 = [0-9]*/UpgradeBlockHeight int64 = $height/" "$upgrade_file" - log_success "Updated upgrade height in $upgrade_file" - else - log_warn "upgrade.go not found at $upgrade_file" - fi + log_info "Setting consensus switch height to $height (via CONSENSUS_SWITCH_HEIGHT env)..." + export CONSENSUS_SWITCH_HEIGHT="$height" + log_success "CONSENSUS_SWITCH_HEIGHT=$height (will be passed to containers)" } # Build test images (with -test suffix) @@ -234,6 +229,7 @@ with open(env_file, 'r+') as envfile: env_data['MORPH_PORTAL'] = addresses['Proxy__L1MessageQueueWithGasPriceOracle'] env_data['MORPH_ROLLUP'] = addresses['Proxy__Rollup'] env_data['MORPH_L1STAKING'] = addresses['Proxy__L1Staking'] + env_data['L1_SEQUENCER_CONTRACT'] = addresses.get('Proxy__L1Sequencer', '') envfile.seek(0) for key, value in env_data.items(): envfile.write(f'{key}={value}\n') @@ -537,8 +533,8 @@ case "${1:-}" in echo " status - Show current block numbers" echo " upgrade-height N - Set upgrade height to N" echo "" - echo "Environment Variables:" - echo " UPGRADE_HEIGHT - Block height for upgrade (default: 50)" + echo "Environment Variables:" + echo " UPGRADE_HEIGHT - Block height for consensus switch (default: 10)" echo " TX_INTERVAL - Seconds between txs (default: 5)" echo "" echo "Test Flow:" @@ -548,6 +544,6 @@ case "${1:-}" in echo " 4. test - Run PBFT -> Upgrade -> Sequencer -> Fullnode tests" echo "" echo "Quick Start:" - echo " UPGRADE_HEIGHT=50 $0 test" + echo " UPGRADE_HEIGHT=10 $0 test" ;; esac diff --git a/ops/l2-genesis/go.mod b/ops/l2-genesis/go.mod index 54625e1be..e464485c4 100644 --- a/ops/l2-genesis/go.mod +++ b/ops/l2-genesis/go.mod @@ -6,7 +6,7 @@ replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3. require ( github.com/holiman/uint256 v1.2.4 - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 github.com/stretchr/testify v1.10.0 github.com/urfave/cli v1.22.17 ) @@ -44,7 +44,7 @@ require ( github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect @@ -52,7 +52,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/tsdb v0.10.0 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/rs/cors v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/ops/l2-genesis/go.sum b/ops/l2-genesis/go.sum index 34798fa73..6907ac512 100644 --- a/ops/l2-genesis/go.sum +++ b/ops/l2-genesis/go.sum @@ -128,8 +128,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -141,8 +141,8 @@ github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqky github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 h1:FUv9gtnvF+1AVrkoNGYbVOesi7E+STjdfD2mcqVaEY0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 h1:A8eygErKU6WKMipGWIemzwLeYkIGLd9yb/Ry3x+J9PQ= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -177,8 +177,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= diff --git a/ops/tools/go.mod b/ops/tools/go.mod index 031d413b2..78ad604a6 100644 --- a/ops/tools/go.mod +++ b/ops/tools/go.mod @@ -5,7 +5,7 @@ go 1.24.0 replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.3 require ( - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 github.com/tendermint/tendermint v0.35.9 ) @@ -47,7 +47,7 @@ require ( github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -62,7 +62,7 @@ require ( github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/tsdb v0.10.0 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/rs/cors v1.11.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect diff --git a/ops/tools/go.sum b/ops/tools/go.sum index 175de8ac4..6be6c6bf0 100644 --- a/ops/tools/go.sum +++ b/ops/tools/go.sum @@ -145,8 +145,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= @@ -163,8 +163,8 @@ github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqky github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 h1:FUv9gtnvF+1AVrkoNGYbVOesi7E+STjdfD2mcqVaEY0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 h1:A8eygErKU6WKMipGWIemzwLeYkIGLd9yb/Ry3x+J9PQ= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= github.com/morph-l2/tendermint v0.3.3 h1:zsmzVJfKp+NuCr45ZUUY2ZJjnHAVLzwJLID6GxBR4i4= github.com/morph-l2/tendermint v0.3.3/go.mod h1:TtCzp9l6Z6yDUiwv3TbqKqw8Q8RKp3fSz5+adO1/Y8w= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -212,8 +212,8 @@ github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3c github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= diff --git a/oracle/go.mod b/oracle/go.mod index 2286651f0..d82290072 100644 --- a/oracle/go.mod +++ b/oracle/go.mod @@ -7,7 +7,7 @@ replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3. require ( github.com/go-kit/kit v0.12.0 github.com/morph-l2/externalsign v0.3.1 - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 github.com/prometheus/client_golang v1.17.0 github.com/stretchr/testify v1.10.0 github.com/tendermint/tendermint v0.35.9 @@ -54,7 +54,7 @@ require ( github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -69,7 +69,7 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/tsdb v0.10.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/rs/cors v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/oracle/go.sum b/oracle/go.sum index 15719693c..da0d78add 100644 --- a/oracle/go.sum +++ b/oracle/go.sum @@ -154,8 +154,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= @@ -174,8 +174,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morph-l2/externalsign v0.3.1 h1:UYFDZFB0L85A4rDvuwLNBiGEi0kSmg9AZ2v8Q5O4dQo= github.com/morph-l2/externalsign v0.3.1/go.mod h1:b6NJ4GUiiG/gcSJsp3p8ExsIs4ZdphlrVALASnVoGJE= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 h1:FUv9gtnvF+1AVrkoNGYbVOesi7E+STjdfD2mcqVaEY0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 h1:A8eygErKU6WKMipGWIemzwLeYkIGLd9yb/Ry3x+J9PQ= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= github.com/morph-l2/tendermint v0.3.3 h1:zsmzVJfKp+NuCr45ZUUY2ZJjnHAVLzwJLID6GxBR4i4= github.com/morph-l2/tendermint v0.3.3/go.mod h1:TtCzp9l6Z6yDUiwv3TbqKqw8Q8RKp3fSz5+adO1/Y8w= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -225,8 +225,8 @@ github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSg github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= diff --git a/token-price-oracle/client/gas.go b/token-price-oracle/client/gas.go new file mode 100644 index 000000000..2d1c4892d --- /dev/null +++ b/token-price-oracle/client/gas.go @@ -0,0 +1,76 @@ +package client + +import ( + "context" + "fmt" + "math/big" + + "github.com/morph-l2/go-ethereum/log" +) + +// GasCaps holds the calculated gas tip cap and gas fee cap +type GasCaps struct { + TipCap *big.Int + FeeCap *big.Int +} + +// CalculateGasCaps calculates dynamic gas caps with optional max limits. +// It fetches the suggested tip and base fee from the network, applies configured +// max limits, and ensures the EIP-1559 invariant (tipCap <= feeCap) is maintained. +func CalculateGasCaps(ctx context.Context, client *L2Client) (*GasCaps, error) { + maxTipCap := client.GetMaxGasTipCap() + maxFeeCap := client.GetMaxGasFeeCap() + + return doCalculateGasCaps(ctx, client, maxTipCap, maxFeeCap) +} + +func doCalculateGasCaps(ctx context.Context, client *L2Client, maxTipCap, maxFeeCap *big.Int) (*GasCaps, error) { + + // Get dynamic gas tip cap from network + tip, err := client.GetClient().SuggestGasTipCap(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get suggested gas tip cap: %w", err) + } + + // Apply tip cap limit if configured + if maxTipCap != nil && tip.Cmp(maxTipCap) > 0 { + log.Debug("Applying gas tip cap limit", "dynamic", tip, "max", maxTipCap) + tip = new(big.Int).Set(maxTipCap) + } + + // Get base fee from latest block + head, err := client.GetClient().HeaderByNumber(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to get block header: %w", err) + } + + // Calculate dynamic gas fee cap: tip + baseFee * 2 + var feeCap *big.Int + if head.BaseFee != nil { + feeCap = new(big.Int).Add( + tip, + new(big.Int).Mul(head.BaseFee, big.NewInt(2)), + ) + } else { + feeCap = new(big.Int).Set(tip) + } + + // Apply fee cap limit if configured + if maxFeeCap != nil && feeCap.Cmp(maxFeeCap) > 0 { + log.Debug("Applying gas fee cap limit", "dynamic", feeCap, "max", maxFeeCap) + feeCap = new(big.Int).Set(maxFeeCap) + } + + // Ensure tipCap <= feeCap (EIP-1559 invariant) + if tip.Cmp(feeCap) > 0 { + log.Debug("Clamping tip to feeCap for EIP-1559 invariant", "tip", tip, "feeCap", feeCap) + tip = new(big.Int).Set(feeCap) + } + + log.Debug("Gas caps calculated", "tipCap", tip, "feeCap", feeCap) + + return &GasCaps{ + TipCap: tip, + FeeCap: feeCap, + }, nil +} diff --git a/token-price-oracle/client/l2_client.go b/token-price-oracle/client/l2_client.go index a69ea044c..1568b068c 100644 --- a/token-price-oracle/client/l2_client.go +++ b/token-price-oracle/client/l2_client.go @@ -22,6 +22,8 @@ type L2Client struct { opts *bind.TransactOpts signer *Signer externalSign bool + gasFeeCap *big.Int // Max gas fee cap (nil means no cap) + gasTipCap *big.Int // Max gas tip cap (nil means no cap) } // NewL2Client creates new L2 client @@ -50,6 +52,16 @@ func NewL2Client(rpcURL string, cfg *config.Config) (*L2Client, error) { externalSign: cfg.ExternalSign, } + // Set gas fee caps if configured (used as max cap, not fixed value) + if cfg.GasFeeCap != nil { + l2Client.gasFeeCap = new(big.Int).SetUint64(*cfg.GasFeeCap) + log.Info("Using gas fee cap limit", "maxGasFeeCap", *cfg.GasFeeCap) + } + if cfg.GasTipCap != nil { + l2Client.gasTipCap = new(big.Int).SetUint64(*cfg.GasTipCap) + log.Info("Using gas tip cap limit", "maxGasTipCap", *cfg.GasTipCap) + } + if cfg.ExternalSign { // External sign mode rsaPriv, err := externalsign.ParseRsaPrivateKey(cfg.ExternalSignRsaPriv) @@ -127,14 +139,14 @@ func (c *L2Client) GetClient() *ethclient.Client { // GetOpts returns a copy of transaction options // Returns a new instance to prevent concurrent modification +// Note: Gas caps are applied by TxManager.applyGasCaps(), not here func (c *L2Client) GetOpts() *bind.TransactOpts { - // Return a copy to prevent shared state issues return &bind.TransactOpts{ - From: c.opts.From, - Nonce: c.opts.Nonce, - Signer: c.opts.Signer, - Value: c.opts.Value, - GasPrice: c.opts.GasPrice, + From: c.opts.From, + Nonce: c.opts.Nonce, + Signer: c.opts.Signer, + Value: c.opts.Value, + GasPrice: c.opts.GasPrice, GasFeeCap: c.opts.GasFeeCap, GasTipCap: c.opts.GasTipCap, GasLimit: c.opts.GasLimit, @@ -167,3 +179,13 @@ func (c *L2Client) GetSigner() *Signer { func (c *L2Client) GetChainID() *big.Int { return c.chainID } + +// GetMaxGasFeeCap returns the max gas fee cap (nil if not configured) +func (c *L2Client) GetMaxGasFeeCap() *big.Int { + return c.gasFeeCap +} + +// GetMaxGasTipCap returns the max gas tip cap (nil if not configured) +func (c *L2Client) GetMaxGasTipCap() *big.Int { + return c.gasTipCap +} diff --git a/token-price-oracle/client/sign.go b/token-price-oracle/client/sign.go index 340e32140..392879a4f 100644 --- a/token-price-oracle/client/sign.go +++ b/token-price-oracle/client/sign.go @@ -96,34 +96,18 @@ func (s *Signer) CreateAndSignTx( return nil, fmt.Errorf("failed to get nonce: %w", err) } - // Get gas tip cap - tip, err := client.GetClient().SuggestGasTipCap(ctx) + // Calculate gas caps (dynamic values with optional max limits) + caps, err := CalculateGasCaps(ctx, client) if err != nil { - return nil, fmt.Errorf("failed to get gas tip cap: %w", err) - } - - // Get base fee from latest block - head, err := client.GetClient().HeaderByNumber(ctx, nil) - if err != nil { - return nil, fmt.Errorf("failed to get block header: %w", err) - } - - var gasFeeCap *big.Int - if head.BaseFee != nil { - gasFeeCap = new(big.Int).Add( - tip, - new(big.Int).Mul(head.BaseFee, big.NewInt(2)), - ) - } else { - gasFeeCap = new(big.Int).Set(tip) + return nil, fmt.Errorf("failed to calculate gas caps: %w", err) } // Estimate gas gas, err := client.GetClient().EstimateGas(ctx, ethereum.CallMsg{ From: from, To: &to, - GasFeeCap: gasFeeCap, - GasTipCap: tip, + GasFeeCap: caps.FeeCap, + GasTipCap: caps.TipCap, Data: callData, }) if err != nil { @@ -137,8 +121,8 @@ func (s *Signer) CreateAndSignTx( tx := types.NewTx(&types.DynamicFeeTx{ ChainID: s.chainID, Nonce: nonce, - GasTipCap: tip, - GasFeeCap: gasFeeCap, + GasTipCap: caps.TipCap, + GasFeeCap: caps.FeeCap, Gas: gas, To: &to, Data: callData, @@ -149,10 +133,9 @@ func (s *Signer) CreateAndSignTx( "to", to.Hex(), "nonce", nonce, "gas", gas, - "gasFeeCap", gasFeeCap, - "gasTipCap", tip) + "gasFeeCap", caps.FeeCap, + "gasTipCap", caps.TipCap) // Sign transaction return s.Sign(tx) } - diff --git a/token-price-oracle/config/config.go b/token-price-oracle/config/config.go index c66b69e9b..ef0923326 100644 --- a/token-price-oracle/config/config.go +++ b/token-price-oracle/config/config.go @@ -85,6 +85,10 @@ type Config struct { LogFileMaxSize int LogFileMaxAge int LogCompress bool + + // Gas fee caps (optional - if set, use as max cap) + GasFeeCap *uint64 // Max gas fee cap in wei (nil means no cap) + GasTipCap *uint64 // Max gas tip cap in wei (nil means no cap) } // LoadConfig loads configuration from cli.Context @@ -112,6 +116,21 @@ func LoadConfig(ctx *cli.Context) (*Config, error) { LogCompress: ctx.Bool(flags.LogCompressFlag.Name), } + // Gas fee caps (only set if flag is explicitly provided) + if ctx.IsSet(flags.GasFeeCapFlag.Name) { + v := ctx.Uint64(flags.GasFeeCapFlag.Name) + cfg.GasFeeCap = &v + } + if ctx.IsSet(flags.GasTipCapFlag.Name) { + v := ctx.Uint64(flags.GasTipCapFlag.Name) + cfg.GasTipCap = &v + } + + // Validate GasFeeCap >= GasTipCap when both are set (EIP-1559 invariant) + if cfg.GasFeeCap != nil && cfg.GasTipCap != nil && *cfg.GasFeeCap < *cfg.GasTipCap { + return nil, fmt.Errorf("--gas-fee-cap (%d) must be >= --gas-tip-cap (%d)", *cfg.GasFeeCap, *cfg.GasTipCap) + } + // Parse token registry address (optional) cfg.L2TokenRegistryAddr = predeploys.L2TokenRegistryAddr diff --git a/token-price-oracle/flags/flags.go b/token-price-oracle/flags/flags.go index 785783e15..1692806b7 100644 --- a/token-price-oracle/flags/flags.go +++ b/token-price-oracle/flags/flags.go @@ -176,6 +176,19 @@ var ( Usage: "The RSA private key for external sign", EnvVar: prefixEnvVar("EXTERNAL_SIGN_RSA_PRIV"), } + + // Gas fee flags (optional - if set, use as max cap instead of dynamic) + GasFeeCapFlag = cli.Uint64Flag{ + Name: "gas-fee-cap", + Usage: "Max gas fee cap in wei (if set, actual fee = min(dynamic, this value))", + EnvVar: prefixEnvVar("GAS_FEE_CAP"), + } + + GasTipCapFlag = cli.Uint64Flag{ + Name: "gas-tip-cap", + Usage: "Max gas tip cap in wei (if set, actual tip = min(dynamic, this value))", + EnvVar: prefixEnvVar("GAS_TIP_CAP"), + } ) var requiredFlags = []cli.Flag{ @@ -210,6 +223,10 @@ var optionalFlags = []cli.Flag{ ExternalSignChainFlag, ExternalSignUrlFlag, ExternalSignRsaPrivFlag, + + // Gas fee + GasFeeCapFlag, + GasTipCapFlag, } // Flags contains the list of configuration options available to the binary. diff --git a/token-price-oracle/go.mod b/token-price-oracle/go.mod index 135e77688..02292b9e8 100644 --- a/token-price-oracle/go.mod +++ b/token-price-oracle/go.mod @@ -9,7 +9,7 @@ replace ( require ( github.com/morph-l2/externalsign v0.3.1 - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 github.com/prometheus/client_golang v1.17.0 github.com/sirupsen/logrus v1.9.3 github.com/urfave/cli v1.22.17 @@ -52,7 +52,7 @@ require ( github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect @@ -63,7 +63,7 @@ require ( github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/tsdb v0.10.0 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/rs/cors v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/token-price-oracle/go.sum b/token-price-oracle/go.sum index bd174e5ae..a0b161877 100644 --- a/token-price-oracle/go.sum +++ b/token-price-oracle/go.sum @@ -130,8 +130,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= @@ -147,8 +147,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morph-l2/externalsign v0.3.1 h1:UYFDZFB0L85A4rDvuwLNBiGEi0kSmg9AZ2v8Q5O4dQo= github.com/morph-l2/externalsign v0.3.1/go.mod h1:b6NJ4GUiiG/gcSJsp3p8ExsIs4ZdphlrVALASnVoGJE= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 h1:FUv9gtnvF+1AVrkoNGYbVOesi7E+STjdfD2mcqVaEY0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 h1:A8eygErKU6WKMipGWIemzwLeYkIGLd9yb/Ry3x+J9PQ= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -191,8 +191,8 @@ github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3c github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= diff --git a/token-price-oracle/updater/tx_manager.go b/token-price-oracle/updater/tx_manager.go index ebed49395..0ae0c8417 100644 --- a/token-price-oracle/updater/tx_manager.go +++ b/token-price-oracle/updater/tx_manager.go @@ -72,6 +72,11 @@ func (m *TxManager) sendWithLocalSign(ctx context.Context, txFunc func(*bind.Tra auth := m.l2Client.GetOpts() auth.Context = ctx + // Apply gas caps if configured (same logic as external sign) + if err := m.applyGasCaps(ctx, auth); err != nil { + return nil, fmt.Errorf("failed to apply gas caps: %w", err) + } + // First, estimate gas with GasLimit = 0 auth.GasLimit = 0 auth.NoSend = true @@ -193,6 +198,19 @@ func (m *TxManager) sendWithExternalSign(ctx context.Context, txFunc func(*bind. return receipt, nil } +// applyGasCaps applies configured gas caps as upper limits to dynamic gas prices +// This ensures consistent behavior between local sign and external sign +func (m *TxManager) applyGasCaps(ctx context.Context, auth *bind.TransactOpts) error { + caps, err := client.CalculateGasCaps(ctx, m.l2Client) + if err != nil { + return err + } + + auth.GasTipCap = caps.TipCap + auth.GasFeeCap = caps.FeeCap + return nil +} + // waitForReceipt waits for a transaction receipt with timeout and custom polling interval func (m *TxManager) waitForReceipt(ctx context.Context, txHash common.Hash, timeout, pollInterval time.Duration) (*types.Receipt, error) { deadline := time.Now().Add(timeout) diff --git a/tx-submitter/go.mod b/tx-submitter/go.mod index 6b1e3cb31..ead642c78 100644 --- a/tx-submitter/go.mod +++ b/tx-submitter/go.mod @@ -9,7 +9,7 @@ require ( github.com/crate-crypto/go-eth-kzg v1.4.0 github.com/holiman/uint256 v1.2.4 github.com/morph-l2/externalsign v0.3.1 - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 github.com/prometheus/client_golang v1.17.0 github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a @@ -51,7 +51,7 @@ require ( github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect @@ -63,7 +63,7 @@ require ( github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/tsdb v0.10.0 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/rs/cors v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/tx-submitter/go.sum b/tx-submitter/go.sum index 63d152c58..725f75a79 100644 --- a/tx-submitter/go.sum +++ b/tx-submitter/go.sum @@ -146,8 +146,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= @@ -163,8 +163,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morph-l2/externalsign v0.3.1 h1:UYFDZFB0L85A4rDvuwLNBiGEi0kSmg9AZ2v8Q5O4dQo= github.com/morph-l2/externalsign v0.3.1/go.mod h1:b6NJ4GUiiG/gcSJsp3p8ExsIs4ZdphlrVALASnVoGJE= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 h1:FUv9gtnvF+1AVrkoNGYbVOesi7E+STjdfD2mcqVaEY0= -github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141 h1:A8eygErKU6WKMipGWIemzwLeYkIGLd9yb/Ry3x+J9PQ= +github.com/morph-l2/go-ethereum v1.10.14-0.20260211074551-4f0f6e6bd141/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -209,8 +209,8 @@ github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3c github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= diff --git a/tx-submitter/services/rollup.go b/tx-submitter/services/rollup.go index caa5471a4..3999477b2 100644 --- a/tx-submitter/services/rollup.go +++ b/tx-submitter/services/rollup.go @@ -833,10 +833,9 @@ func (r *Rollup) finalize() error { log.Info("batch inside challenge window, wait") return nil } - // finalize - // get next batch nextBatchIndex := target.Uint64() + 1 + batch, err := GetRollupBatchByIndex(nextBatchIndex, r.L2Clients) if err != nil { log.Error("get next batch by index error",