From 3c38d6f9474ef26542745736a5b0655b856864bb Mon Sep 17 00:00:00 2001 From: amit-momin Date: Thu, 18 Jan 2024 12:23:23 -0600 Subject: [PATCH 1/6] Added L1 gas cost estimation feature to L1 gas oracle for Arbitrum, Optimism, and Scroll --- core/chains/evm/gas/models.go | 2 +- .../evm/gas/rollups/l1_gas_price_oracle.go | 196 ------------ .../gas/rollups/l1_gas_price_oracle_test.go | 125 -------- core/chains/evm/gas/rollups/l1_oracle.go | 284 ++++++++++++++++++ core/chains/evm/gas/rollups/l1_oracle_test.go | 254 ++++++++++++++++ .../chains/evm/gas/rollups/mocks/l1_oracle.go | 36 ++- core/chains/evm/gas/rollups/models.go | 4 + 7 files changed, 578 insertions(+), 323 deletions(-) delete mode 100644 core/chains/evm/gas/rollups/l1_gas_price_oracle.go delete mode 100644 core/chains/evm/gas/rollups/l1_gas_price_oracle_test.go create mode 100644 core/chains/evm/gas/rollups/l1_oracle.go create mode 100644 core/chains/evm/gas/rollups/l1_oracle_test.go diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index 8d977df0991..8fe7e3d0e8c 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -69,7 +69,7 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, ge // create l1Oracle only if it is supported for the chain var l1Oracle rollups.L1Oracle if rollups.IsRollupWithL1Support(cfg.ChainType()) { - l1Oracle = rollups.NewL1GasPriceOracle(lggr, ethClient, cfg.ChainType()) + l1Oracle = rollups.NewL1GasOracle(lggr, ethClient, cfg.ChainType()) } var newEstimator func(logger.Logger) EvmEstimator switch s { diff --git a/core/chains/evm/gas/rollups/l1_gas_price_oracle.go b/core/chains/evm/gas/rollups/l1_gas_price_oracle.go deleted file mode 100644 index a006ea89923..00000000000 --- a/core/chains/evm/gas/rollups/l1_gas_price_oracle.go +++ /dev/null @@ -1,196 +0,0 @@ -package rollups - -import ( - "context" - "errors" - "fmt" - "math/big" - "slices" - "sync" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink-common/pkg/utils" - - "github.com/smartcontractkit/chainlink/v2/common/config" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" -) - -//go:generate mockery --quiet --name ethClient --output ./mocks/ --case=underscore --structname ETHClient -type ethClient interface { - CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) -} - -// Reads L2-specific precompiles and caches the l1GasPrice set by the L2. -type l1GasPriceOracle struct { - services.StateMachine - client ethClient - pollPeriod time.Duration - logger logger.SugaredLogger - address string - callArgs string - - l1GasPriceMu sync.RWMutex - l1GasPrice *assets.Wei - - chInitialised chan struct{} - chStop services.StopChan - chDone chan struct{} -} - -const ( - // ArbGasInfoAddress is the address of the "Precompiled contract that exists in every Arbitrum chain." - // https://github.com/OffchainLabs/nitro/blob/f7645453cfc77bf3e3644ea1ac031eff629df325/contracts/src/precompiles/ArbGasInfo.sol - ArbGasInfoAddress = "0x000000000000000000000000000000000000006C" - // ArbGasInfo_getL1BaseFeeEstimate is the a hex encoded call to: - // `function getL1BaseFeeEstimate() external view returns (uint256);` - ArbGasInfo_getL1BaseFeeEstimate = "f5d6ded7" - - // GasOracleAddress is the address of the precompiled contract that exists on OP stack chain. - // This is the case for Optimism and Base. - OPGasOracleAddress = "0x420000000000000000000000000000000000000F" - // GasOracle_l1BaseFee is the a hex encoded call to: - // `function l1BaseFee() external view returns (uint256);` - OPGasOracle_l1BaseFee = "519b4bd3" - - // GasOracleAddress is the address of the precompiled contract that exists on Kroma chain. - // This is the case for Kroma. - KromaGasOracleAddress = "0x4200000000000000000000000000000000000005" - // GasOracle_l1BaseFee is the a hex encoded call to: - // `function l1BaseFee() external view returns (uint256);` - KromaGasOracle_l1BaseFee = "519b4bd3" - - // GasOracleAddress is the address of the precompiled contract that exists on scroll chain. - // This is the case for Scroll. - ScrollGasOracleAddress = "0x5300000000000000000000000000000000000002" - // GasOracle_l1BaseFee is the a hex encoded call to: - // `function l1BaseFee() external view returns (uint256);` - ScrollGasOracle_l1BaseFee = "519b4bd3" - - // Interval at which to poll for L1BaseFee. A good starting point is the L1 block time. - PollPeriod = 12 * time.Second -) - -var supportedChainTypes = []config.ChainType{config.ChainArbitrum, config.ChainOptimismBedrock, config.ChainKroma, config.ChainScroll} - -func IsRollupWithL1Support(chainType config.ChainType) bool { - return slices.Contains(supportedChainTypes, chainType) -} - -func NewL1GasPriceOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType) L1Oracle { - var address, callArgs string - switch chainType { - case config.ChainArbitrum: - address = ArbGasInfoAddress - callArgs = ArbGasInfo_getL1BaseFeeEstimate - case config.ChainOptimismBedrock: - address = OPGasOracleAddress - callArgs = OPGasOracle_l1BaseFee - case config.ChainKroma: - address = KromaGasOracleAddress - callArgs = KromaGasOracle_l1BaseFee - case config.ChainScroll: - address = ScrollGasOracleAddress - callArgs = ScrollGasOracle_l1BaseFee - default: - panic(fmt.Sprintf("Received unspported chaintype %s", chainType)) - } - - return &l1GasPriceOracle{ - client: ethClient, - pollPeriod: PollPeriod, - logger: logger.Sugared(logger.Named(lggr, fmt.Sprintf("L1GasPriceOracle(%s)", chainType))), - address: address, - callArgs: callArgs, - chInitialised: make(chan struct{}), - chStop: make(chan struct{}), - chDone: make(chan struct{}), - } -} - -func (o *l1GasPriceOracle) Name() string { - return o.logger.Name() -} - -func (o *l1GasPriceOracle) Start(ctx context.Context) error { - return o.StartOnce(o.Name(), func() error { - go o.run() - <-o.chInitialised - return nil - }) -} -func (o *l1GasPriceOracle) Close() error { - return o.StopOnce(o.Name(), func() error { - close(o.chStop) - <-o.chDone - return nil - }) -} - -func (o *l1GasPriceOracle) HealthReport() map[string]error { - return map[string]error{o.Name(): o.Healthy()} -} - -func (o *l1GasPriceOracle) run() { - defer close(o.chDone) - - t := o.refresh() - close(o.chInitialised) - - for { - select { - case <-o.chStop: - return - case <-t.C: - t = o.refresh() - } - } -} - -func (o *l1GasPriceOracle) refresh() (t *time.Timer) { - t = time.NewTimer(utils.WithJitter(o.pollPeriod)) - - ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) - defer cancel() - - precompile := common.HexToAddress(o.address) - b, err := o.client.CallContract(ctx, ethereum.CallMsg{ - To: &precompile, - Data: common.Hex2Bytes(o.callArgs), - }, nil) - if err != nil { - o.logger.Errorf("gas oracle contract call failed: %v", err) - return - } - - if len(b) != 32 { // returns uint256; - o.logger.Criticalf("return data length (%d) different than expected (%d)", len(b), 32) - return - } - price := new(big.Int).SetBytes(b) - - o.l1GasPriceMu.Lock() - defer o.l1GasPriceMu.Unlock() - o.l1GasPrice = assets.NewWei(price) - return -} - -func (o *l1GasPriceOracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) { - ok := o.IfStarted(func() { - o.l1GasPriceMu.RLock() - l1GasPrice = o.l1GasPrice - o.l1GasPriceMu.RUnlock() - }) - if !ok { - return l1GasPrice, errors.New("L1GasPriceOracle is not started; cannot estimate gas") - } - if l1GasPrice == nil { - return l1GasPrice, errors.New("failed to get l1 gas price; gas price not set") - } - return -} diff --git a/core/chains/evm/gas/rollups/l1_gas_price_oracle_test.go b/core/chains/evm/gas/rollups/l1_gas_price_oracle_test.go deleted file mode 100644 index 188376bf832..00000000000 --- a/core/chains/evm/gas/rollups/l1_gas_price_oracle_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package rollups - -import ( - "fmt" - "math/big" - "testing" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" - - "github.com/smartcontractkit/chainlink/v2/common/config" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -func TestL1GasPriceOracle(t *testing.T) { - t.Parallel() - - t.Run("Unsupported ChainType returns nil", func(t *testing.T) { - ethClient := mocks.NewETHClient(t) - - assert.Panicsf(t, func() { NewL1GasPriceOracle(logger.Test(t), ethClient, config.ChainCelo) }, "Received unspported chaintype %s", config.ChainCelo) - }) - - t.Run("Calling L1GasPrice on unstarted L1Oracle returns error", func(t *testing.T) { - ethClient := mocks.NewETHClient(t) - - oracle := NewL1GasPriceOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock) - - _, err := oracle.GasPrice(testutils.Context(t)) - assert.EqualError(t, err, "L1GasPriceOracle is not started; cannot estimate gas") - }) - - t.Run("Calling GasPrice on started Arbitrum L1Oracle returns Arbitrum l1GasPrice", func(t *testing.T) { - l1BaseFee := big.NewInt(100) - - ethClient := mocks.NewETHClient(t) - ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { - callMsg := args.Get(1).(ethereum.CallMsg) - blockNumber := args.Get(2).(*big.Int) - assert.Equal(t, ArbGasInfoAddress, callMsg.To.String()) - assert.Equal(t, ArbGasInfo_getL1BaseFeeEstimate, fmt.Sprintf("%x", callMsg.Data)) - assert.Nil(t, blockNumber) - }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) - - oracle := NewL1GasPriceOracle(logger.Test(t), ethClient, config.ChainArbitrum) - servicetest.RunHealthy(t, oracle) - - gasPrice, err := oracle.GasPrice(testutils.Context(t)) - require.NoError(t, err) - - assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) - }) - - t.Run("Calling GasPrice on started Kroma L1Oracle returns Kroma l1GasPrice", func(t *testing.T) { - l1BaseFee := big.NewInt(200) - - ethClient := mocks.NewETHClient(t) - ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { - callMsg := args.Get(1).(ethereum.CallMsg) - blockNumber := args.Get(2).(*big.Int) - assert.Equal(t, KromaGasOracleAddress, callMsg.To.String()) - assert.Equal(t, KromaGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) - assert.Nil(t, blockNumber) - }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) - - oracle := NewL1GasPriceOracle(logger.Test(t), ethClient, config.ChainKroma) - servicetest.RunHealthy(t, oracle) - - gasPrice, err := oracle.GasPrice(testutils.Context(t)) - require.NoError(t, err) - - assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) - }) - - t.Run("Calling GasPrice on started OPStack L1Oracle returns OPStack l1GasPrice", func(t *testing.T) { - l1BaseFee := big.NewInt(200) - - ethClient := mocks.NewETHClient(t) - ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { - callMsg := args.Get(1).(ethereum.CallMsg) - blockNumber := args.Get(2).(*big.Int) - assert.Equal(t, OPGasOracleAddress, callMsg.To.String()) - assert.Equal(t, OPGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) - assert.Nil(t, blockNumber) - }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) - - oracle := NewL1GasPriceOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock) - servicetest.RunHealthy(t, oracle) - - gasPrice, err := oracle.GasPrice(testutils.Context(t)) - require.NoError(t, err) - - assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) - }) - - t.Run("Calling GasPrice on started Scroll L1Oracle returns Scroll l1GasPrice", func(t *testing.T) { - l1BaseFee := big.NewInt(200) - - ethClient := mocks.NewETHClient(t) - ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { - callMsg := args.Get(1).(ethereum.CallMsg) - blockNumber := args.Get(2).(*big.Int) - assert.Equal(t, ScrollGasOracleAddress, callMsg.To.String()) - assert.Equal(t, ScrollGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) - assert.Nil(t, blockNumber) - }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) - - oracle := NewL1GasPriceOracle(logger.Test(t), ethClient, config.ChainScroll) - require.NoError(t, oracle.Start(testutils.Context(t))) - t.Cleanup(func() { assert.NoError(t, oracle.Close()) }) - - gasPrice, err := oracle.GasPrice(testutils.Context(t)) - require.NoError(t, err) - - assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) - }) -} diff --git a/core/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go new file mode 100644 index 00000000000..aad3e9202ac --- /dev/null +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -0,0 +1,284 @@ +package rollups + +import ( + "context" + "errors" + "fmt" + "math/big" + "slices" + "sync" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/utils" + + gethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/smartcontractkit/chainlink/v2/common/config" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" +) + +//go:generate mockery --quiet --name ethClient --output ./mocks/ --case=underscore --structname ETHClient +type ethClient interface { + CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) +} + +// Reads L2-specific precompiles and caches the l1GasPrice set by the L2. +type l1Oracle struct { + services.StateMachine + client ethClient + pollPeriod time.Duration + logger logger.SugaredLogger + chainType config.ChainType + + l1GasPriceAddress string + gasPriceMethodHash string + l1GasPriceMu sync.RWMutex + l1GasPrice *assets.Wei + + l1GasCostAddress string + gasCostMethodHash string + + chInitialised chan struct{} + chStop services.StopChan + chDone chan struct{} +} + +const ( + // ArbGasInfoAddress is the address of the "Precompiled contract that exists in every Arbitrum chain." + // https://github.com/OffchainLabs/nitro/blob/f7645453cfc77bf3e3644ea1ac031eff629df325/contracts/src/precompiles/ArbGasInfo.sol + ArbGasInfoAddress = "0x000000000000000000000000000000000000006C" + // ArbGasInfo_getL1BaseFeeEstimate is the a hex encoded call to: + // `function getL1BaseFeeEstimate() external view returns (uint256);` + ArbGasInfo_getL1BaseFeeEstimate = "f5d6ded7" + // NodeInterfaceAddress is the address of the precompiled contract that is only available through RPC + // https://github.com/OffchainLabs/nitro/blob/e815395d2e91fb17f4634cad72198f6de79c6e61/nodeInterface/NodeInterface.go#L37 + ArbNodeInterfaceAddress = "0x00000000000000000000000000000000000000C8" + // ArbGasInfo_getPricesInArbGas is the a hex encoded call to: + // `function gasEstimateL1Component(address to, bool contractCreation, bytes calldata data) external payable returns (uint64 gasEstimateForL1, uint256 baseFee, uint256 l1BaseFeeEstimate);` + ArbNodeInterface_gasEstimateL1Component = "77d488a2" + + // OPGasOracleAddress is the address of the precompiled contract that exists on OP stack chain. + // This is the case for Optimism and Base. + OPGasOracleAddress = "0x420000000000000000000000000000000000000F" + // OPGasOracle_l1BaseFee is a hex encoded call to: + // `function l1BaseFee() external view returns (uint256);` + OPGasOracle_l1BaseFee = "519b4bd3" + // OPGasOracle_getL1Fee is a hex encoded call to: + // `function getL1Fee(bytes) external view returns (uint256);` + OPGasOracle_getL1Fee = "49948e0e" + + // ScrollGasOracleAddress is the address of the precompiled contract that exists on Scroll chain. + ScrollGasOracleAddress = "0x5300000000000000000000000000000000000002" + // ScrollGasOracle_l1BaseFee is a hex encoded call to: + // `function l1BaseFee() external view returns (uint256);` + ScrollGasOracle_l1BaseFee = "519b4bd3" + // ScrollGasOracle_getL1Fee is a hex encoded call to: + // `function getL1Fee(bytes) external view returns (uint256);` + ScrollGasOracle_getL1Fee = "49948e0e" + + // GasOracleAddress is the address of the precompiled contract that exists on Kroma chain. + // This is the case for Kroma. + KromaGasOracleAddress = "0x4200000000000000000000000000000000000005" + // GasOracle_l1BaseFee is the a hex encoded call to: + // `function l1BaseFee() external view returns (uint256);` + KromaGasOracle_l1BaseFee = "519b4bd3" + + // Interval at which to poll for L1BaseFee. A good starting point is the L1 block time. + PollPeriod = 12 * time.Second + + // RPC call timeout + queryTimeout = 10 * time.Second +) + +var supportedChainTypes = []config.ChainType{config.ChainArbitrum, config.ChainOptimismBedrock, config.ChainKroma, config.ChainScroll} + +func IsRollupWithL1Support(chainType config.ChainType) bool { + return slices.Contains(supportedChainTypes, chainType) +} + +func NewL1GasOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType) L1Oracle { + var l1GasPriceAddress, gasPriceMethodHash, l1GasCostAddress, gasCostMethodHash string + switch chainType { + case config.ChainArbitrum: + l1GasPriceAddress = ArbGasInfoAddress + gasPriceMethodHash = ArbGasInfo_getL1BaseFeeEstimate + l1GasCostAddress = ArbNodeInterfaceAddress + gasCostMethodHash = ArbNodeInterface_gasEstimateL1Component + case config.ChainOptimismBedrock: + l1GasPriceAddress = OPGasOracleAddress + gasPriceMethodHash = OPGasOracle_l1BaseFee + l1GasCostAddress = OPGasOracleAddress + gasCostMethodHash = OPGasOracle_getL1Fee + case config.ChainKroma: + l1GasPriceAddress = KromaGasOracleAddress + gasPriceMethodHash = KromaGasOracle_l1BaseFee + l1GasCostAddress = "" + gasCostMethodHash = "" + case config.ChainScroll: + l1GasPriceAddress = ScrollGasOracleAddress + gasPriceMethodHash = ScrollGasOracle_l1BaseFee + l1GasCostAddress = ScrollGasOracleAddress + gasCostMethodHash = ScrollGasOracle_getL1Fee + default: + panic(fmt.Sprintf("Received unspported chaintype %s", chainType)) + } + + return &l1Oracle{ + client: ethClient, + pollPeriod: PollPeriod, + logger: logger.Sugared(logger.Named(lggr, fmt.Sprintf("L1GasOracle(%s)", chainType))), + chainType: chainType, + + l1GasPriceAddress: l1GasPriceAddress, + gasPriceMethodHash: gasPriceMethodHash, + l1GasCostAddress: l1GasCostAddress, + gasCostMethodHash: gasCostMethodHash, + + chInitialised: make(chan struct{}), + chStop: make(chan struct{}), + chDone: make(chan struct{}), + } +} + +func (o *l1Oracle) Name() string { + return o.logger.Name() +} + +func (o *l1Oracle) Start(ctx context.Context) error { + return o.StartOnce(o.Name(), func() error { + go o.run() + <-o.chInitialised + return nil + }) +} +func (o *l1Oracle) Close() error { + return o.StopOnce(o.Name(), func() error { + close(o.chStop) + <-o.chDone + return nil + }) +} + +func (o *l1Oracle) HealthReport() map[string]error { + return map[string]error{o.Name(): o.Healthy()} +} + +func (o *l1Oracle) run() { + defer close(o.chDone) + + t := o.refresh() + close(o.chInitialised) + + for { + select { + case <-o.chStop: + return + case <-t.C: + t = o.refresh() + } + } +} + +func (o *l1Oracle) refresh() (t *time.Timer) { + t = time.NewTimer(utils.WithJitter(o.pollPeriod)) + + ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + precompile := common.HexToAddress(o.l1GasPriceAddress) + b, err := o.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: common.Hex2Bytes(o.gasPriceMethodHash), + }, nil) + if err != nil { + o.logger.Errorf("gas oracle contract call failed: %v", err) + return + } + + if len(b) != 32 { // returns uint256; + o.logger.Criticalf("return data length (%d) different than expected (%d)", len(b), 32) + return + } + price := new(big.Int).SetBytes(b) + + o.l1GasPriceMu.Lock() + defer o.l1GasPriceMu.Unlock() + o.l1GasPrice = assets.NewWei(price) + return +} + +func (o *l1Oracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) { + ok := o.IfStarted(func() { + o.l1GasPriceMu.RLock() + l1GasPrice = o.l1GasPrice + o.l1GasPriceMu.RUnlock() + }) + if !ok { + return l1GasPrice, errors.New("L1GasOracle is not started; cannot estimate gas") + } + if l1GasPrice == nil { + return l1GasPrice, errors.New("failed to get l1 gas price; gas price not set") + } + return +} + +// Gets the L1 gas cost for the provided transaction at the specified block num +// If block num is not provided, the value on the latest block num is used +func (o *l1Oracle) GetGasCost(ctx context.Context, tx *gethtypes.Transaction, blockNum *big.Int) (*assets.Wei, error) { + ctx, cancel := context.WithTimeout(ctx, queryTimeout) + defer cancel() + callArgs := common.Hex2Bytes(o.gasCostMethodHash) + if o.chainType == config.ChainOptimismBedrock || o.chainType == config.ChainScroll { + // Append rlp-encoded tx + encodedtx, err := tx.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to marshal tx for gas cost estimation: %w", err) + } + callArgs = append(callArgs, encodedtx...) + } else if o.chainType == config.ChainArbitrum { + // Append To address + callArgs = append(callArgs, tx.To().Bytes()...) + // Append bool if contract creation (always false for our use case) + callArgs = append(callArgs, byte(0)) + // Append calldata + callArgs = append(callArgs, tx.Data()...) + } else { + return nil, fmt.Errorf("L1 gas cost not supported for this chain: %s", o.chainType) + } + + precompile := common.HexToAddress(o.l1GasCostAddress) + b, err := o.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: callArgs, + }, blockNum) + if err != nil { + errorMsg := fmt.Sprintf("gas oracle contract call failed: %v", err) + o.logger.Errorf(errorMsg) + return nil, fmt.Errorf(errorMsg) + } + + var l1GasCost *big.Int + if o.chainType == config.ChainOptimismBedrock || o.chainType == config.ChainScroll { + if len(b) != 32 { // returns uint256; + errorMsg := fmt.Sprintf("return data length (%d) different than expected (%d)", len(b), 32) + o.logger.Critical(errorMsg) + return nil, fmt.Errorf(errorMsg) + } + l1GasCost = new(big.Int).SetBytes(b) + } else if o.chainType == config.ChainArbitrum { + if len(b) != 8+2*32 { // returns (uint64 gasEstimateForL1, uint256 baseFee, uint256 l1BaseFeeEstimate); + errorMsg := fmt.Sprintf("return data length (%d) different than expected (%d)", len(b), 8+2*32) + o.logger.Critical(errorMsg) + return nil, fmt.Errorf(errorMsg) + } + l1GasCost = new(big.Int).SetBytes(b[:8]) + } + + return assets.NewWei(l1GasCost), nil +} diff --git a/core/chains/evm/gas/rollups/l1_oracle_test.go b/core/chains/evm/gas/rollups/l1_oracle_test.go new file mode 100644 index 00000000000..24b69f1b7fd --- /dev/null +++ b/core/chains/evm/gas/rollups/l1_oracle_test.go @@ -0,0 +1,254 @@ +package rollups + +import ( + "fmt" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" + + "github.com/smartcontractkit/chainlink/v2/common/config" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" +) + +func TestL1Oracle(t *testing.T) { + t.Parallel() + + t.Run("Unsupported ChainType returns nil", func(t *testing.T) { + ethClient := mocks.NewETHClient(t) + + assert.Panicsf(t, func() { NewL1GasOracle(logger.Test(t), ethClient, config.ChainCelo) }, "Received unspported chaintype %s", config.ChainCelo) + }) +} + +func TestL1Oracle_GasPrice(t *testing.T) { + t.Parallel() + + t.Run("Calling GasPrice on unstarted L1Oracle returns error", func(t *testing.T) { + ethClient := mocks.NewETHClient(t) + + oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock) + + _, err := oracle.GasPrice(testutils.Context(t)) + assert.EqualError(t, err, "L1GasOracle is not started; cannot estimate gas") + }) + + t.Run("Calling GasPrice on started Arbitrum L1Oracle returns Arbitrum l1GasPrice", func(t *testing.T) { + l1BaseFee := big.NewInt(100) + + ethClient := mocks.NewETHClient(t) + ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + callMsg := args.Get(1).(ethereum.CallMsg) + blockNumber := args.Get(2).(*big.Int) + assert.Equal(t, ArbGasInfoAddress, callMsg.To.String()) + assert.Equal(t, ArbGasInfo_getL1BaseFeeEstimate, fmt.Sprintf("%x", callMsg.Data)) + assert.Nil(t, blockNumber) + }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) + + oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainArbitrum) + servicetest.RunHealthy(t, oracle) + + gasPrice, err := oracle.GasPrice(testutils.Context(t)) + require.NoError(t, err) + + assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) + }) + + t.Run("Calling GasPrice on started Kroma L1Oracle returns Kroma l1GasPrice", func(t *testing.T) { + l1BaseFee := big.NewInt(100) + + ethClient := mocks.NewETHClient(t) + ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + callMsg := args.Get(1).(ethereum.CallMsg) + blockNumber := args.Get(2).(*big.Int) + assert.Equal(t, KromaGasOracleAddress, callMsg.To.String()) + assert.Equal(t, KromaGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) + assert.Nil(t, blockNumber) + }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) + + oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainKroma) + servicetest.RunHealthy(t, oracle) + + gasPrice, err := oracle.GasPrice(testutils.Context(t)) + require.NoError(t, err) + + assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) + }) + + t.Run("Calling GasPrice on started OPStack L1Oracle returns OPStack l1GasPrice", func(t *testing.T) { + l1BaseFee := big.NewInt(100) + + ethClient := mocks.NewETHClient(t) + ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + callMsg := args.Get(1).(ethereum.CallMsg) + blockNumber := args.Get(2).(*big.Int) + assert.Equal(t, OPGasOracleAddress, callMsg.To.String()) + assert.Equal(t, OPGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) + assert.Nil(t, blockNumber) + }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) + + oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock) + servicetest.RunHealthy(t, oracle) + + gasPrice, err := oracle.GasPrice(testutils.Context(t)) + require.NoError(t, err) + + assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) + }) + + t.Run("Calling GasPrice on started Scroll L1Oracle returns Scroll l1GasPrice", func(t *testing.T) { + l1BaseFee := big.NewInt(200) + + ethClient := mocks.NewETHClient(t) + ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + callMsg := args.Get(1).(ethereum.CallMsg) + blockNumber := args.Get(2).(*big.Int) + assert.Equal(t, ScrollGasOracleAddress, callMsg.To.String()) + assert.Equal(t, ScrollGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) + assert.Nil(t, blockNumber) + }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) + + oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainScroll) + require.NoError(t, oracle.Start(testutils.Context(t))) + t.Cleanup(func() { assert.NoError(t, oracle.Close()) }) + + gasPrice, err := oracle.GasPrice(testutils.Context(t)) + require.NoError(t, err) + + assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) + }) +} + +func TestL1Oracle_GetGasCost(t *testing.T) { + t.Parallel() + + t.Run("Calling GetGasCost on started Arbitrum L1Oracle returns Arbitrum getL1Fee", func(t *testing.T) { + l1GasCost := big.NewInt(100) + baseFee := utils.Uint256ToBytes32(big.NewInt(1000)) + l1BaseFeeEstimate := utils.Uint256ToBytes32(big.NewInt(500)) + blockNum := big.NewInt(1000) + toAddress := utils.RandomAddress() + callData := []byte{1, 2, 3, 4, 5, 6, 7} + + tx := types.NewTx(&types.LegacyTx{ + Nonce: 42, + To: &toAddress, + Data: callData, + }) + result := common.LeftPadBytes(l1GasCost.Bytes(), 8) + result = append(result, baseFee...) + result = append(result, l1BaseFeeEstimate...) + + ethClient := mocks.NewETHClient(t) + ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + callMsg := args.Get(1).(ethereum.CallMsg) + blockNumber := args.Get(2).(*big.Int) + methodHash := callMsg.Data[:4] + to := callMsg.Data[4:24] + contractCreation := callMsg.Data[24] + data := callMsg.Data[25:32] + require.Equal(t, ArbNodeInterfaceAddress, callMsg.To.String()) + require.Equal(t, ArbNodeInterface_gasEstimateL1Component, fmt.Sprintf("%x", methodHash)) + require.Equal(t, toAddress, common.BytesToAddress(to)) + require.Equal(t, byte(0), contractCreation) + require.Equal(t, callData, data) + require.Equal(t, blockNum, blockNumber) + }).Return(result, nil) + + oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainArbitrum) + + gasCost, err := oracle.GetGasCost(testutils.Context(t), tx, blockNum) + require.NoError(t, err) + require.Equal(t, assets.NewWei(l1GasCost), gasCost) + }) + + t.Run("Calling GetGasCost on started Kroma L1Oracle returns error", func(t *testing.T) { + blockNum := big.NewInt(1000) + tx := types.NewTx(&types.LegacyTx{}) + + ethClient := mocks.NewETHClient(t) + oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainKroma) + + _, err := oracle.GetGasCost(testutils.Context(t), tx, blockNum) + require.Error(t, err, "L1 gas cost not supported for this chain: kroma") + }) + + t.Run("Calling GetGasCost on started OPStack L1Oracle returns OPStack getL1Fee", func(t *testing.T) { + l1GasCost := big.NewInt(100) + blockNum := big.NewInt(1000) + toAddress := utils.RandomAddress() + callData := []byte{1, 2, 3} + + tx := types.NewTx(&types.LegacyTx{ + Nonce: 42, + To: &toAddress, + Data: callData, + }) + + encodedTx, err := tx.MarshalBinary() + require.NoError(t, err) + + ethClient := mocks.NewETHClient(t) + ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + callMsg := args.Get(1).(ethereum.CallMsg) + blockNumber := args.Get(2).(*big.Int) + methodHash := callMsg.Data[:4] + encodedTxInCall := callMsg.Data[4 : len(encodedTx)+4] + require.Equal(t, OPGasOracleAddress, callMsg.To.String()) + require.Equal(t, OPGasOracle_getL1Fee, fmt.Sprintf("%x", methodHash)) + require.Equal(t, encodedTx, encodedTxInCall) + require.Equal(t, blockNum, blockNumber) + }).Return(common.BigToHash(l1GasCost).Bytes(), nil) + + oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock) + + gasCost, err := oracle.GetGasCost(testutils.Context(t), tx, blockNum) + require.NoError(t, err) + require.Equal(t, assets.NewWei(l1GasCost), gasCost) + }) + + t.Run("Calling GetGasCost on started Scroll L1Oracle returns Scroll getL1Fee", func(t *testing.T) { + l1GasCost := big.NewInt(100) + blockNum := big.NewInt(1000) + toAddress := utils.RandomAddress() + callData := []byte{1, 2, 3} + + tx := types.NewTx(&types.LegacyTx{ + Nonce: 42, + To: &toAddress, + Data: callData, + }) + + encodedTx, err := tx.MarshalBinary() + require.NoError(t, err) + + ethClient := mocks.NewETHClient(t) + ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + callMsg := args.Get(1).(ethereum.CallMsg) + blockNumber := args.Get(2).(*big.Int) + methodHash := callMsg.Data[:4] + encodedTxInCall := callMsg.Data[4 : len(encodedTx)+4] + require.Equal(t, ScrollGasOracleAddress, callMsg.To.String()) + require.Equal(t, ScrollGasOracle_getL1Fee, fmt.Sprintf("%x", methodHash)) + require.Equal(t, encodedTx, encodedTxInCall) + require.Equal(t, blockNum, blockNumber) + }).Return(common.BigToHash(l1GasCost).Bytes(), nil) + + oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainScroll) + + gasCost, err := oracle.GetGasCost(testutils.Context(t), tx, blockNum) + require.NoError(t, err) + require.Equal(t, assets.NewWei(l1GasCost), gasCost) + }) +} diff --git a/core/chains/evm/gas/rollups/mocks/l1_oracle.go b/core/chains/evm/gas/rollups/mocks/l1_oracle.go index 9e52a3ec38e..101090c0594 100644 --- a/core/chains/evm/gas/rollups/mocks/l1_oracle.go +++ b/core/chains/evm/gas/rollups/mocks/l1_oracle.go @@ -3,11 +3,15 @@ package mocks import ( - context "context" + big "math/big" assets "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + context "context" + mock "github.com/stretchr/testify/mock" + + types "github.com/ethereum/go-ethereum/core/types" ) // L1Oracle is an autogenerated mock type for the L1Oracle type @@ -63,6 +67,36 @@ func (_m *L1Oracle) GasPrice(ctx context.Context) (*assets.Wei, error) { return r0, r1 } +// GetGasCost provides a mock function with given fields: ctx, tx, blockNum +func (_m *L1Oracle) GetGasCost(ctx context.Context, tx *types.Transaction, blockNum *big.Int) (*assets.Wei, error) { + ret := _m.Called(ctx, tx, blockNum) + + if len(ret) == 0 { + panic("no return value specified for GetGasCost") + } + + var r0 *assets.Wei + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction, *big.Int) (*assets.Wei, error)); ok { + return rf(ctx, tx, blockNum) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction, *big.Int) *assets.Wei); ok { + r0 = rf(ctx, tx, blockNum) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*assets.Wei) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.Transaction, *big.Int) error); ok { + r1 = rf(ctx, tx, blockNum) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // HealthReport provides a mock function with given fields: func (_m *L1Oracle) HealthReport() map[string]error { ret := _m.Called() diff --git a/core/chains/evm/gas/rollups/models.go b/core/chains/evm/gas/rollups/models.go index 1659436071b..8158ba2b906 100644 --- a/core/chains/evm/gas/rollups/models.go +++ b/core/chains/evm/gas/rollups/models.go @@ -2,6 +2,9 @@ package rollups import ( "context" + "math/big" + + "github.com/ethereum/go-ethereum/core/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/services" @@ -15,4 +18,5 @@ type L1Oracle interface { services.ServiceCtx GasPrice(ctx context.Context) (*assets.Wei, error) + GetGasCost(ctx context.Context, tx *types.Transaction, blockNum *big.Int) (*assets.Wei, error) } From 8db0a672b3e2e8d4aadb1ff9f46660bf1b7963ac Mon Sep 17 00:00:00 2001 From: amit-momin Date: Fri, 19 Jan 2024 15:47:29 -0600 Subject: [PATCH 2/6] Updated implementation to use ABIs to pack payloads --- core/chains/evm/gas/rollups/l1_oracle.go | 109 +++++++++++------- core/chains/evm/gas/rollups/l1_oracle_abi.go | 9 ++ core/chains/evm/gas/rollups/l1_oracle_test.go | 65 ++++++----- 3 files changed, 115 insertions(+), 68 deletions(-) create mode 100644 core/chains/evm/gas/rollups/l1_oracle_abi.go diff --git a/core/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go index aad3e9202ac..2462e25aa7f 100644 --- a/core/chains/evm/gas/rollups/l1_oracle.go +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -6,10 +6,12 @@ import ( "fmt" "math/big" "slices" + "strings" "sync" "time" "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -36,13 +38,15 @@ type l1Oracle struct { logger logger.SugaredLogger chainType config.ChainType - l1GasPriceAddress string - gasPriceMethodHash string - l1GasPriceMu sync.RWMutex - l1GasPrice *assets.Wei + l1GasPriceAddress string + gasPriceMethod string + l1GasPriceMethodAbi abi.ABI + l1GasPriceMu sync.RWMutex + l1GasPrice *assets.Wei - l1GasCostAddress string - gasCostMethodHash string + l1GasCostAddress string + gasCostMethod string + l1GasCostMethodAbi abi.ABI chInitialised chan struct{} chStop services.StopChan @@ -55,39 +59,39 @@ const ( ArbGasInfoAddress = "0x000000000000000000000000000000000000006C" // ArbGasInfo_getL1BaseFeeEstimate is the a hex encoded call to: // `function getL1BaseFeeEstimate() external view returns (uint256);` - ArbGasInfo_getL1BaseFeeEstimate = "f5d6ded7" + ArbGasInfo_getL1BaseFeeEstimate = "getL1BaseFeeEstimate" // NodeInterfaceAddress is the address of the precompiled contract that is only available through RPC // https://github.com/OffchainLabs/nitro/blob/e815395d2e91fb17f4634cad72198f6de79c6e61/nodeInterface/NodeInterface.go#L37 ArbNodeInterfaceAddress = "0x00000000000000000000000000000000000000C8" // ArbGasInfo_getPricesInArbGas is the a hex encoded call to: // `function gasEstimateL1Component(address to, bool contractCreation, bytes calldata data) external payable returns (uint64 gasEstimateForL1, uint256 baseFee, uint256 l1BaseFeeEstimate);` - ArbNodeInterface_gasEstimateL1Component = "77d488a2" + ArbNodeInterface_gasEstimateL1Component = "gasEstimateL1Component" // OPGasOracleAddress is the address of the precompiled contract that exists on OP stack chain. // This is the case for Optimism and Base. OPGasOracleAddress = "0x420000000000000000000000000000000000000F" // OPGasOracle_l1BaseFee is a hex encoded call to: // `function l1BaseFee() external view returns (uint256);` - OPGasOracle_l1BaseFee = "519b4bd3" + OPGasOracle_l1BaseFee = "l1BaseFee" // OPGasOracle_getL1Fee is a hex encoded call to: // `function getL1Fee(bytes) external view returns (uint256);` - OPGasOracle_getL1Fee = "49948e0e" + OPGasOracle_getL1Fee = "getL1Fee" // ScrollGasOracleAddress is the address of the precompiled contract that exists on Scroll chain. ScrollGasOracleAddress = "0x5300000000000000000000000000000000000002" // ScrollGasOracle_l1BaseFee is a hex encoded call to: // `function l1BaseFee() external view returns (uint256);` - ScrollGasOracle_l1BaseFee = "519b4bd3" + ScrollGasOracle_l1BaseFee = "l1BaseFee" // ScrollGasOracle_getL1Fee is a hex encoded call to: // `function getL1Fee(bytes) external view returns (uint256);` - ScrollGasOracle_getL1Fee = "49948e0e" + ScrollGasOracle_getL1Fee = "getL1Fee" // GasOracleAddress is the address of the precompiled contract that exists on Kroma chain. // This is the case for Kroma. KromaGasOracleAddress = "0x4200000000000000000000000000000000000005" // GasOracle_l1BaseFee is the a hex encoded call to: // `function l1BaseFee() external view returns (uint256);` - KromaGasOracle_l1BaseFee = "519b4bd3" + KromaGasOracle_l1BaseFee = "l1BaseFee" // Interval at which to poll for L1BaseFee. A good starting point is the L1 block time. PollPeriod = 12 * time.Second @@ -103,42 +107,60 @@ func IsRollupWithL1Support(chainType config.ChainType) bool { } func NewL1GasOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType) L1Oracle { - var l1GasPriceAddress, gasPriceMethodHash, l1GasCostAddress, gasCostMethodHash string + var l1GasPriceAddress, gasPriceMethod, l1GasCostAddress, gasCostMethod string + var l1GasPriceMethodAbi, l1GasCostMethodAbi abi.ABI + var gasPriceErr, gasCostErr error switch chainType { case config.ChainArbitrum: l1GasPriceAddress = ArbGasInfoAddress - gasPriceMethodHash = ArbGasInfo_getL1BaseFeeEstimate + gasPriceMethod = ArbGasInfo_getL1BaseFeeEstimate + l1GasPriceMethodAbi, gasPriceErr = abi.JSON(strings.NewReader(GetL1BaseFeeEstimateAbiString)) l1GasCostAddress = ArbNodeInterfaceAddress - gasCostMethodHash = ArbNodeInterface_gasEstimateL1Component + gasCostMethod = ArbNodeInterface_gasEstimateL1Component + l1GasCostMethodAbi, gasCostErr = abi.JSON(strings.NewReader(GasEstimateL1ComponentAbiString)) case config.ChainOptimismBedrock: l1GasPriceAddress = OPGasOracleAddress - gasPriceMethodHash = OPGasOracle_l1BaseFee + gasPriceMethod = OPGasOracle_l1BaseFee + l1GasPriceMethodAbi, gasPriceErr = abi.JSON(strings.NewReader(L1BaseFeeAbiString)) l1GasCostAddress = OPGasOracleAddress - gasCostMethodHash = OPGasOracle_getL1Fee + gasCostMethod = OPGasOracle_getL1Fee + l1GasCostMethodAbi, gasCostErr = abi.JSON(strings.NewReader(GetL1FeeAbiString)) case config.ChainKroma: l1GasPriceAddress = KromaGasOracleAddress - gasPriceMethodHash = KromaGasOracle_l1BaseFee + gasPriceMethod = KromaGasOracle_l1BaseFee + l1GasPriceMethodAbi, gasPriceErr = abi.JSON(strings.NewReader(L1BaseFeeAbiString)) l1GasCostAddress = "" - gasCostMethodHash = "" + gasCostMethod = "" case config.ChainScroll: l1GasPriceAddress = ScrollGasOracleAddress - gasPriceMethodHash = ScrollGasOracle_l1BaseFee + gasPriceMethod = ScrollGasOracle_l1BaseFee + l1GasPriceMethodAbi, gasPriceErr = abi.JSON(strings.NewReader(L1BaseFeeAbiString)) l1GasCostAddress = ScrollGasOracleAddress - gasCostMethodHash = ScrollGasOracle_getL1Fee + gasCostMethod = ScrollGasOracle_getL1Fee + l1GasCostMethodAbi, gasCostErr = abi.JSON(strings.NewReader(GetL1FeeAbiString)) default: panic(fmt.Sprintf("Received unspported chaintype %s", chainType)) } + if gasPriceErr != nil { + panic(fmt.Sprintf("Failed to parse L1 gas price method ABI for chain: %s", chainType)) + } + if gasCostErr != nil { + panic(fmt.Sprintf("Failed to parse L1 gas cost method ABI for chain: %s", chainType)) + } + return &l1Oracle{ client: ethClient, pollPeriod: PollPeriod, logger: logger.Sugared(logger.Named(lggr, fmt.Sprintf("L1GasOracle(%s)", chainType))), chainType: chainType, - l1GasPriceAddress: l1GasPriceAddress, - gasPriceMethodHash: gasPriceMethodHash, - l1GasCostAddress: l1GasCostAddress, - gasCostMethodHash: gasCostMethodHash, + l1GasPriceAddress: l1GasPriceAddress, + gasPriceMethod: gasPriceMethod, + l1GasPriceMethodAbi: l1GasPriceMethodAbi, + l1GasCostAddress: l1GasCostAddress, + gasCostMethod: gasCostMethod, + l1GasCostMethodAbi: l1GasCostMethodAbi, chInitialised: make(chan struct{}), chStop: make(chan struct{}), @@ -191,10 +213,17 @@ func (o *l1Oracle) refresh() (t *time.Timer) { ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) defer cancel() + var callData, b []byte + var err error precompile := common.HexToAddress(o.l1GasPriceAddress) - b, err := o.client.CallContract(ctx, ethereum.CallMsg{ + callData, err = o.l1GasPriceMethodAbi.Pack(o.gasPriceMethod) + if err != nil { + o.logger.Errorf("failed to pack calldata for %s L1 gas price method: %w", o.chainType, err) + return + } + b, err = o.client.CallContract(ctx, ethereum.CallMsg{ To: &precompile, - Data: common.Hex2Bytes(o.gasPriceMethodHash), + Data: callData, }, nil) if err != nil { o.logger.Errorf("gas oracle contract call failed: %v", err) @@ -233,29 +262,29 @@ func (o *l1Oracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err erro func (o *l1Oracle) GetGasCost(ctx context.Context, tx *gethtypes.Transaction, blockNum *big.Int) (*assets.Wei, error) { ctx, cancel := context.WithTimeout(ctx, queryTimeout) defer cancel() - callArgs := common.Hex2Bytes(o.gasCostMethodHash) + var callData, b []byte + var err error if o.chainType == config.ChainOptimismBedrock || o.chainType == config.ChainScroll { // Append rlp-encoded tx - encodedtx, err := tx.MarshalBinary() - if err != nil { + var encodedtx []byte + if encodedtx, err = tx.MarshalBinary(); err != nil { return nil, fmt.Errorf("failed to marshal tx for gas cost estimation: %w", err) } - callArgs = append(callArgs, encodedtx...) + if callData, err = o.l1GasCostMethodAbi.Pack(o.gasCostMethod, encodedtx); err != nil { + return nil, fmt.Errorf("failed to pack calldata for %s L1 gas cost estimation method: %w", o.chainType, err) + } } else if o.chainType == config.ChainArbitrum { - // Append To address - callArgs = append(callArgs, tx.To().Bytes()...) - // Append bool if contract creation (always false for our use case) - callArgs = append(callArgs, byte(0)) - // Append calldata - callArgs = append(callArgs, tx.Data()...) + if callData, err = o.l1GasCostMethodAbi.Pack(o.gasCostMethod, tx.To(), false, tx.Data()); err != nil { + return nil, fmt.Errorf("failed to pack calldata for %s L1 gas cost estimation method: %w", o.chainType, err) + } } else { return nil, fmt.Errorf("L1 gas cost not supported for this chain: %s", o.chainType) } precompile := common.HexToAddress(o.l1GasCostAddress) - b, err := o.client.CallContract(ctx, ethereum.CallMsg{ + b, err = o.client.CallContract(ctx, ethereum.CallMsg{ To: &precompile, - Data: callArgs, + Data: callData, }, blockNum) if err != nil { errorMsg := fmt.Sprintf("gas oracle contract call failed: %v", err) diff --git a/core/chains/evm/gas/rollups/l1_oracle_abi.go b/core/chains/evm/gas/rollups/l1_oracle_abi.go new file mode 100644 index 00000000000..61cf55c2c50 --- /dev/null +++ b/core/chains/evm/gas/rollups/l1_oracle_abi.go @@ -0,0 +1,9 @@ +package rollups + +/* ABIs for Arbitrum Gas Info and Node Interface precompile contract methods needed for the L1 oracle */ +const GetL1BaseFeeEstimateAbiString = `[{"inputs":[],"name":"getL1BaseFeeEstimate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` +const GasEstimateL1ComponentAbiString = `[{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"bool","name":"contractCreation","type":"bool"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"gasEstimateL1Component","outputs":[{"internalType":"uint64","name":"gasEstimateForL1","type":"uint64"},{"internalType":"uint256","name":"baseFee","type":"uint256"},{"internalType":"uint256","name":"l1BaseFeeEstimate","type":"uint256"}],"stateMutability":"payable","type":"function"}]` + +/* ABIs for Optimism, Scroll, and Kroma precompile contract methods needed for the L1 oracle */ +const L1BaseFeeAbiString = `[{"inputs":[],"name":"l1BaseFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` +const GetL1FeeAbiString = `[{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"getL1Fee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` diff --git a/core/chains/evm/gas/rollups/l1_oracle_test.go b/core/chains/evm/gas/rollups/l1_oracle_test.go index 24b69f1b7fd..ec44049340d 100644 --- a/core/chains/evm/gas/rollups/l1_oracle_test.go +++ b/core/chains/evm/gas/rollups/l1_oracle_test.go @@ -1,11 +1,12 @@ package rollups import ( - "fmt" "math/big" + "strings" "testing" "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/assert" @@ -46,13 +47,16 @@ func TestL1Oracle_GasPrice(t *testing.T) { t.Run("Calling GasPrice on started Arbitrum L1Oracle returns Arbitrum l1GasPrice", func(t *testing.T) { l1BaseFee := big.NewInt(100) + l1GasPriceMethodAbi, err := abi.JSON(strings.NewReader(GetL1BaseFeeEstimateAbiString)) + require.NoError(t, err) ethClient := mocks.NewETHClient(t) ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - assert.Equal(t, ArbGasInfoAddress, callMsg.To.String()) - assert.Equal(t, ArbGasInfo_getL1BaseFeeEstimate, fmt.Sprintf("%x", callMsg.Data)) + payload, err := l1GasPriceMethodAbi.Pack("getL1BaseFeeEstimate") + require.NoError(t, err) + require.Equal(t, payload, callMsg.Data) assert.Nil(t, blockNumber) }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) @@ -67,13 +71,16 @@ func TestL1Oracle_GasPrice(t *testing.T) { t.Run("Calling GasPrice on started Kroma L1Oracle returns Kroma l1GasPrice", func(t *testing.T) { l1BaseFee := big.NewInt(100) + l1GasPriceMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString)) + require.NoError(t, err) ethClient := mocks.NewETHClient(t) ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - assert.Equal(t, KromaGasOracleAddress, callMsg.To.String()) - assert.Equal(t, KromaGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) + payload, err := l1GasPriceMethodAbi.Pack("l1BaseFee") + require.NoError(t, err) + require.Equal(t, payload, callMsg.Data) assert.Nil(t, blockNumber) }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) @@ -88,13 +95,16 @@ func TestL1Oracle_GasPrice(t *testing.T) { t.Run("Calling GasPrice on started OPStack L1Oracle returns OPStack l1GasPrice", func(t *testing.T) { l1BaseFee := big.NewInt(100) + l1GasPriceMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString)) + require.NoError(t, err) ethClient := mocks.NewETHClient(t) ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - assert.Equal(t, OPGasOracleAddress, callMsg.To.String()) - assert.Equal(t, OPGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) + payload, err := l1GasPriceMethodAbi.Pack("l1BaseFee") + require.NoError(t, err) + require.Equal(t, payload, callMsg.Data) assert.Nil(t, blockNumber) }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) @@ -109,13 +119,16 @@ func TestL1Oracle_GasPrice(t *testing.T) { t.Run("Calling GasPrice on started Scroll L1Oracle returns Scroll l1GasPrice", func(t *testing.T) { l1BaseFee := big.NewInt(200) + l1GasPriceMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString)) + require.NoError(t, err) ethClient := mocks.NewETHClient(t) ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - assert.Equal(t, ScrollGasOracleAddress, callMsg.To.String()) - assert.Equal(t, ScrollGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) + payload, err := l1GasPriceMethodAbi.Pack("l1BaseFee") + require.NoError(t, err) + require.Equal(t, payload, callMsg.Data) assert.Nil(t, blockNumber) }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) @@ -140,6 +153,8 @@ func TestL1Oracle_GetGasCost(t *testing.T) { blockNum := big.NewInt(1000) toAddress := utils.RandomAddress() callData := []byte{1, 2, 3, 4, 5, 6, 7} + l1GasCostMethodAbi, err := abi.JSON(strings.NewReader(GasEstimateL1ComponentAbiString)) + require.NoError(t, err) tx := types.NewTx(&types.LegacyTx{ Nonce: 42, @@ -154,15 +169,9 @@ func TestL1Oracle_GetGasCost(t *testing.T) { ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - methodHash := callMsg.Data[:4] - to := callMsg.Data[4:24] - contractCreation := callMsg.Data[24] - data := callMsg.Data[25:32] - require.Equal(t, ArbNodeInterfaceAddress, callMsg.To.String()) - require.Equal(t, ArbNodeInterface_gasEstimateL1Component, fmt.Sprintf("%x", methodHash)) - require.Equal(t, toAddress, common.BytesToAddress(to)) - require.Equal(t, byte(0), contractCreation) - require.Equal(t, callData, data) + payload, err := l1GasCostMethodAbi.Pack("gasEstimateL1Component", toAddress, false, callData) + require.NoError(t, err) + require.Equal(t, payload, callMsg.Data) require.Equal(t, blockNum, blockNumber) }).Return(result, nil) @@ -189,6 +198,8 @@ func TestL1Oracle_GetGasCost(t *testing.T) { blockNum := big.NewInt(1000) toAddress := utils.RandomAddress() callData := []byte{1, 2, 3} + l1GasCostMethodAbi, err := abi.JSON(strings.NewReader(GetL1FeeAbiString)) + require.NoError(t, err) tx := types.NewTx(&types.LegacyTx{ Nonce: 42, @@ -203,11 +214,9 @@ func TestL1Oracle_GetGasCost(t *testing.T) { ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - methodHash := callMsg.Data[:4] - encodedTxInCall := callMsg.Data[4 : len(encodedTx)+4] - require.Equal(t, OPGasOracleAddress, callMsg.To.String()) - require.Equal(t, OPGasOracle_getL1Fee, fmt.Sprintf("%x", methodHash)) - require.Equal(t, encodedTx, encodedTxInCall) + payload, err := l1GasCostMethodAbi.Pack("getL1Fee", encodedTx) + require.NoError(t, err) + require.Equal(t, payload, callMsg.Data) require.Equal(t, blockNum, blockNumber) }).Return(common.BigToHash(l1GasCost).Bytes(), nil) @@ -223,6 +232,8 @@ func TestL1Oracle_GetGasCost(t *testing.T) { blockNum := big.NewInt(1000) toAddress := utils.RandomAddress() callData := []byte{1, 2, 3} + l1GasCostMethodAbi, err := abi.JSON(strings.NewReader(GetL1FeeAbiString)) + require.NoError(t, err) tx := types.NewTx(&types.LegacyTx{ Nonce: 42, @@ -237,11 +248,9 @@ func TestL1Oracle_GetGasCost(t *testing.T) { ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - methodHash := callMsg.Data[:4] - encodedTxInCall := callMsg.Data[4 : len(encodedTx)+4] - require.Equal(t, ScrollGasOracleAddress, callMsg.To.String()) - require.Equal(t, ScrollGasOracle_getL1Fee, fmt.Sprintf("%x", methodHash)) - require.Equal(t, encodedTx, encodedTxInCall) + payload, err := l1GasCostMethodAbi.Pack("getL1Fee", encodedTx) + require.NoError(t, err) + require.Equal(t, payload, callMsg.Data) require.Equal(t, blockNum, blockNumber) }).Return(common.BigToHash(l1GasCost).Bytes(), nil) From 44931ebf5728500625cd26e6454584af1f50a6d9 Mon Sep 17 00:00:00 2001 From: amit-momin Date: Mon, 22 Jan 2024 13:35:02 -0600 Subject: [PATCH 3/6] Fixed linting --- core/chains/evm/gas/rollups/l1_oracle_test.go | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/core/chains/evm/gas/rollups/l1_oracle_test.go b/core/chains/evm/gas/rollups/l1_oracle_test.go index ec44049340d..8415e4d7805 100644 --- a/core/chains/evm/gas/rollups/l1_oracle_test.go +++ b/core/chains/evm/gas/rollups/l1_oracle_test.go @@ -54,7 +54,8 @@ func TestL1Oracle_GasPrice(t *testing.T) { ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - payload, err := l1GasPriceMethodAbi.Pack("getL1BaseFeeEstimate") + var payload []byte + payload, err = l1GasPriceMethodAbi.Pack("getL1BaseFeeEstimate") require.NoError(t, err) require.Equal(t, payload, callMsg.Data) assert.Nil(t, blockNumber) @@ -78,7 +79,8 @@ func TestL1Oracle_GasPrice(t *testing.T) { ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - payload, err := l1GasPriceMethodAbi.Pack("l1BaseFee") + var payload []byte + payload, err = l1GasPriceMethodAbi.Pack("l1BaseFee") require.NoError(t, err) require.Equal(t, payload, callMsg.Data) assert.Nil(t, blockNumber) @@ -102,7 +104,8 @@ func TestL1Oracle_GasPrice(t *testing.T) { ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - payload, err := l1GasPriceMethodAbi.Pack("l1BaseFee") + var payload []byte + payload, err = l1GasPriceMethodAbi.Pack("l1BaseFee") require.NoError(t, err) require.Equal(t, payload, callMsg.Data) assert.Nil(t, blockNumber) @@ -126,7 +129,8 @@ func TestL1Oracle_GasPrice(t *testing.T) { ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - payload, err := l1GasPriceMethodAbi.Pack("l1BaseFee") + var payload []byte + payload, err = l1GasPriceMethodAbi.Pack("l1BaseFee") require.NoError(t, err) require.Equal(t, payload, callMsg.Data) assert.Nil(t, blockNumber) @@ -169,7 +173,8 @@ func TestL1Oracle_GetGasCost(t *testing.T) { ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - payload, err := l1GasCostMethodAbi.Pack("gasEstimateL1Component", toAddress, false, callData) + var payload []byte + payload, err = l1GasCostMethodAbi.Pack("gasEstimateL1Component", toAddress, false, callData) require.NoError(t, err) require.Equal(t, payload, callMsg.Data) require.Equal(t, blockNum, blockNumber) @@ -214,7 +219,8 @@ func TestL1Oracle_GetGasCost(t *testing.T) { ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - payload, err := l1GasCostMethodAbi.Pack("getL1Fee", encodedTx) + var payload []byte + payload, err = l1GasCostMethodAbi.Pack("getL1Fee", encodedTx) require.NoError(t, err) require.Equal(t, payload, callMsg.Data) require.Equal(t, blockNum, blockNumber) @@ -248,7 +254,8 @@ func TestL1Oracle_GetGasCost(t *testing.T) { ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { callMsg := args.Get(1).(ethereum.CallMsg) blockNumber := args.Get(2).(*big.Int) - payload, err := l1GasCostMethodAbi.Pack("getL1Fee", encodedTx) + var payload []byte + payload, err = l1GasCostMethodAbi.Pack("getL1Fee", encodedTx) require.NoError(t, err) require.Equal(t, payload, callMsg.Data) require.Equal(t, blockNum, blockNumber) From c694cb463a2e4c647614ec817524204934f5ca10 Mon Sep 17 00:00:00 2001 From: amit-momin Date: Wed, 24 Jan 2024 11:21:10 -0600 Subject: [PATCH 4/6] Addressed feedback --- core/chains/evm/gas/rollups/l1_oracle.go | 56 +++++++++++++------- core/chains/evm/gas/rollups/l1_oracle_abi.go | 3 ++ 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/core/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go index 2462e25aa7f..fb2b7894d08 100644 --- a/core/chains/evm/gas/rollups/l1_oracle.go +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -2,7 +2,6 @@ package rollups import ( "context" - "errors" "fmt" "math/big" "slices" @@ -20,6 +19,7 @@ import ( gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/smartcontractkit/chainlink/v2/common/client" "github.com/smartcontractkit/chainlink/v2/common/config" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" @@ -30,6 +30,11 @@ type ethClient interface { CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) } +type priceEntry struct { + price *assets.Wei + timestamp time.Time +} + // Reads L2-specific precompiles and caches the l1GasPrice set by the L2. type l1Oracle struct { services.StateMachine @@ -42,7 +47,7 @@ type l1Oracle struct { gasPriceMethod string l1GasPriceMethodAbi abi.ABI l1GasPriceMu sync.RWMutex - l1GasPrice *assets.Wei + l1GasPrice priceEntry l1GasCostAddress string gasCostMethod string @@ -94,10 +99,7 @@ const ( KromaGasOracle_l1BaseFee = "l1BaseFee" // Interval at which to poll for L1BaseFee. A good starting point is the L1 block time. - PollPeriod = 12 * time.Second - - // RPC call timeout - queryTimeout = 10 * time.Second + PollPeriod = 6 * time.Second ) var supportedChainTypes = []config.ChainType{config.ChainArbitrum, config.ChainOptimismBedrock, config.ChainKroma, config.ChainScroll} @@ -206,53 +208,69 @@ func (o *l1Oracle) run() { } } } - func (o *l1Oracle) refresh() (t *time.Timer) { + t, err := o.refreshWithError() + if err != nil { + o.SvcErrBuffer.Append(err) + } + return +} + +func (o *l1Oracle) refreshWithError() (t *time.Timer, err error) { t = time.NewTimer(utils.WithJitter(o.pollPeriod)) ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) defer cancel() var callData, b []byte - var err error precompile := common.HexToAddress(o.l1GasPriceAddress) callData, err = o.l1GasPriceMethodAbi.Pack(o.gasPriceMethod) if err != nil { - o.logger.Errorf("failed to pack calldata for %s L1 gas price method: %w", o.chainType, err) - return + errMsg := fmt.Sprintf("failed to pack calldata for %s L1 gas price method", o.chainType) + o.logger.Errorf(errMsg) + return t, fmt.Errorf("%s: %w", errMsg, err) } b, err = o.client.CallContract(ctx, ethereum.CallMsg{ To: &precompile, Data: callData, }, nil) if err != nil { - o.logger.Errorf("gas oracle contract call failed: %v", err) - return + errMsg := fmt.Sprint("gas oracle contract call failed") + o.logger.Errorf(errMsg) + return t, fmt.Errorf("%s: %w", errMsg, err) } if len(b) != 32 { // returns uint256; - o.logger.Criticalf("return data length (%d) different than expected (%d)", len(b), 32) - return + errMsg := fmt.Sprintf("return data length (%d) different than expected (%d)", len(b), 32) + o.logger.Criticalf(errMsg) + return t, fmt.Errorf(errMsg) } price := new(big.Int).SetBytes(b) o.l1GasPriceMu.Lock() defer o.l1GasPriceMu.Unlock() - o.l1GasPrice = assets.NewWei(price) + o.l1GasPrice = priceEntry{price: assets.NewWei(price), timestamp: time.Now()} return } func (o *l1Oracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) { + var timestamp time.Time ok := o.IfStarted(func() { o.l1GasPriceMu.RLock() - l1GasPrice = o.l1GasPrice + l1GasPrice = o.l1GasPrice.price + timestamp = o.l1GasPrice.timestamp o.l1GasPriceMu.RUnlock() }) if !ok { - return l1GasPrice, errors.New("L1GasOracle is not started; cannot estimate gas") + return l1GasPrice, fmt.Errorf("L1GasOracle is not started; cannot estimate gas") } if l1GasPrice == nil { - return l1GasPrice, errors.New("failed to get l1 gas price; gas price not set") + return l1GasPrice, fmt.Errorf("failed to get l1 gas price; gas price not set") + } + // Validate the price has been updated within the pollPeriod * 2 + // Allowing double the poll period before declaring the price stale to give ample time for the refresh to process + if time.Since(timestamp) > o.pollPeriod * 2 { + return l1GasPrice, fmt.Errorf("gas price is stale") } return } @@ -260,7 +278,7 @@ func (o *l1Oracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err erro // Gets the L1 gas cost for the provided transaction at the specified block num // If block num is not provided, the value on the latest block num is used func (o *l1Oracle) GetGasCost(ctx context.Context, tx *gethtypes.Transaction, blockNum *big.Int) (*assets.Wei, error) { - ctx, cancel := context.WithTimeout(ctx, queryTimeout) + ctx, cancel := context.WithTimeout(ctx, client.QueryTimeout) defer cancel() var callData, b []byte var err error diff --git a/core/chains/evm/gas/rollups/l1_oracle_abi.go b/core/chains/evm/gas/rollups/l1_oracle_abi.go index 61cf55c2c50..c6e8444d291 100644 --- a/core/chains/evm/gas/rollups/l1_oracle_abi.go +++ b/core/chains/evm/gas/rollups/l1_oracle_abi.go @@ -1,9 +1,12 @@ package rollups /* ABIs for Arbitrum Gas Info and Node Interface precompile contract methods needed for the L1 oracle */ +// ABI found at https://arbiscan.io/address/0x000000000000000000000000000000000000006C#code const GetL1BaseFeeEstimateAbiString = `[{"inputs":[],"name":"getL1BaseFeeEstimate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` +// ABI found at https://arbiscan.io/address/0x00000000000000000000000000000000000000C8#code const GasEstimateL1ComponentAbiString = `[{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"bool","name":"contractCreation","type":"bool"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"gasEstimateL1Component","outputs":[{"internalType":"uint64","name":"gasEstimateForL1","type":"uint64"},{"internalType":"uint256","name":"baseFee","type":"uint256"},{"internalType":"uint256","name":"l1BaseFeeEstimate","type":"uint256"}],"stateMutability":"payable","type":"function"}]` /* ABIs for Optimism, Scroll, and Kroma precompile contract methods needed for the L1 oracle */ +// All ABIs found at https://optimistic.etherscan.io/address/0xc0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d3000f#code const L1BaseFeeAbiString = `[{"inputs":[],"name":"l1BaseFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` const GetL1FeeAbiString = `[{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"getL1Fee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` From b3f78ed809e4ddbaaff0aa62932eeaa965d02f8f Mon Sep 17 00:00:00 2001 From: amit-momin Date: Wed, 24 Jan 2024 11:24:43 -0600 Subject: [PATCH 5/6] Fixed linting --- core/chains/evm/gas/rollups/l1_oracle.go | 6 +++--- core/chains/evm/gas/rollups/l1_oracle_abi.go | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go index fb2b7894d08..b6f1eaae17a 100644 --- a/core/chains/evm/gas/rollups/l1_oracle.go +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -31,8 +31,8 @@ type ethClient interface { } type priceEntry struct { - price *assets.Wei - timestamp time.Time + price *assets.Wei + timestamp time.Time } // Reads L2-specific precompiles and caches the l1GasPrice set by the L2. @@ -269,7 +269,7 @@ func (o *l1Oracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err erro } // Validate the price has been updated within the pollPeriod * 2 // Allowing double the poll period before declaring the price stale to give ample time for the refresh to process - if time.Since(timestamp) > o.pollPeriod * 2 { + if time.Since(timestamp) > o.pollPeriod*2 { return l1GasPrice, fmt.Errorf("gas price is stale") } return diff --git a/core/chains/evm/gas/rollups/l1_oracle_abi.go b/core/chains/evm/gas/rollups/l1_oracle_abi.go index c6e8444d291..77ef4d49f3c 100644 --- a/core/chains/evm/gas/rollups/l1_oracle_abi.go +++ b/core/chains/evm/gas/rollups/l1_oracle_abi.go @@ -3,6 +3,7 @@ package rollups /* ABIs for Arbitrum Gas Info and Node Interface precompile contract methods needed for the L1 oracle */ // ABI found at https://arbiscan.io/address/0x000000000000000000000000000000000000006C#code const GetL1BaseFeeEstimateAbiString = `[{"inputs":[],"name":"getL1BaseFeeEstimate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` + // ABI found at https://arbiscan.io/address/0x00000000000000000000000000000000000000C8#code const GasEstimateL1ComponentAbiString = `[{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"bool","name":"contractCreation","type":"bool"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"gasEstimateL1Component","outputs":[{"internalType":"uint64","name":"gasEstimateForL1","type":"uint64"},{"internalType":"uint256","name":"baseFee","type":"uint256"},{"internalType":"uint256","name":"l1BaseFeeEstimate","type":"uint256"}],"stateMutability":"payable","type":"function"}]` From 6a914729fff52ecc1e6ab274b230ba210982f166 Mon Sep 17 00:00:00 2001 From: amit-momin Date: Wed, 24 Jan 2024 11:32:30 -0600 Subject: [PATCH 6/6] Fixed linting --- core/chains/evm/gas/rollups/l1_oracle.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go index b6f1eaae17a..e9cdc6b73b1 100644 --- a/core/chains/evm/gas/rollups/l1_oracle.go +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -235,7 +235,7 @@ func (o *l1Oracle) refreshWithError() (t *time.Timer, err error) { Data: callData, }, nil) if err != nil { - errMsg := fmt.Sprint("gas oracle contract call failed") + errMsg := "gas oracle contract call failed" o.logger.Errorf(errMsg) return t, fmt.Errorf("%s: %w", errMsg, err) }