diff --git a/.changeset/flat-guests-marry.md b/.changeset/flat-guests-marry.md new file mode 100644 index 00000000000..c1eb6549a96 --- /dev/null +++ b/.changeset/flat-guests-marry.md @@ -0,0 +1,6 @@ +--- +"chainlink": minor +--- + +#internal Gas Estimator L1Oracles to be chain specific +#removed cmd/arbgas diff --git a/core/chains/evm/gas/arbitrum_estimator.go b/core/chains/evm/gas/arbitrum_estimator.go index 40366c5b998..0cd4bbcdd0b 100644 --- a/core/chains/evm/gas/arbitrum_estimator.go +++ b/core/chains/evm/gas/arbitrum_estimator.go @@ -3,14 +3,10 @@ package gas import ( "context" "fmt" - "math" - "math/big" "slices" "sync" "time" - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" pkgerrors "github.com/pkg/errors" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -19,7 +15,7 @@ import ( feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" ) type ArbConfig interface { @@ -28,11 +24,6 @@ type ArbConfig interface { BumpMin() *assets.Wei } -//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) -} - // arbitrumEstimator is an Estimator which extends SuggestedPriceEstimator to use getPricesInArbGas() for gas limit estimation. type arbitrumEstimator struct { services.StateMachine @@ -40,7 +31,6 @@ type arbitrumEstimator struct { EvmEstimator // *SuggestedPriceEstimator - client ethClient pollPeriod time.Duration logger logger.Logger @@ -52,20 +42,23 @@ type arbitrumEstimator struct { chInitialised chan struct{} chStop services.StopChan chDone chan struct{} + + l1Oracle rollups.ArbL1GasOracle } -func NewArbitrumEstimator(lggr logger.Logger, cfg ArbConfig, rpcClient rpcClient, ethClient ethClient) EvmEstimator { +func NewArbitrumEstimator(lggr logger.Logger, cfg ArbConfig, ethClient feeEstimatorClient, l1Oracle rollups.ArbL1GasOracle) EvmEstimator { lggr = logger.Named(lggr, "ArbitrumEstimator") + return &arbitrumEstimator{ cfg: cfg, - EvmEstimator: NewSuggestedPriceEstimator(lggr, rpcClient, cfg), - client: ethClient, + EvmEstimator: NewSuggestedPriceEstimator(lggr, ethClient, cfg, l1Oracle), pollPeriod: 10 * time.Second, logger: lggr, chForceRefetch: make(chan (chan struct{})), chInitialised: make(chan struct{}), chStop: make(chan struct{}), chDone: make(chan struct{}), + l1Oracle: l1Oracle, } } @@ -196,7 +189,7 @@ func (a *arbitrumEstimator) run() { func (a *arbitrumEstimator) refreshPricesInArbGas() (t *time.Timer) { t = time.NewTimer(utils.WithJitter(a.pollPeriod)) - perL2Tx, perL1CalldataUnit, err := a.callGetPricesInArbGas() + perL2Tx, perL1CalldataUnit, err := a.l1Oracle.GetPricesInArbGas() if err != nil { a.logger.Warnw("Failed to refresh prices", "err", err) return @@ -210,54 +203,3 @@ func (a *arbitrumEstimator) refreshPricesInArbGas() (t *time.Timer) { a.getPricesInArbGasMu.Unlock() return } - -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_getPricesInArbGas is the a hex encoded call to: - // `function getPricesInArbGas() external view returns (uint256, uint256, uint256);` - ArbGasInfo_getPricesInArbGas = "02199f34" -) - -// callGetPricesInArbGas calls ArbGasInfo.getPricesInArbGas() on the precompile contract ArbGasInfoAddress. -// -// @return (per L2 tx, per L1 calldata unit, per storage allocation) -// function getPricesInArbGas() external view returns (uint256, uint256, uint256); -// -// https://github.com/OffchainLabs/nitro/blob/f7645453cfc77bf3e3644ea1ac031eff629df325/contracts/src/precompiles/ArbGasInfo.sol#L69 -func (a *arbitrumEstimator) callGetPricesInArbGas() (perL2Tx uint32, perL1CalldataUnit uint32, err error) { - ctx, cancel := a.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) - defer cancel() - - precompile := common.HexToAddress(ArbGasInfoAddress) - b, err := a.client.CallContract(ctx, ethereum.CallMsg{ - To: &precompile, - Data: common.Hex2Bytes(ArbGasInfo_getPricesInArbGas), - }, big.NewInt(-1)) - if err != nil { - return 0, 0, err - } - - if len(b) != 3*32 { // returns (uint256, uint256, uint256); - err = fmt.Errorf("return data length (%d) different than expected (%d)", len(b), 3*32) - return - } - bPerL2Tx := new(big.Int).SetBytes(b[:32]) - bPerL1CalldataUnit := new(big.Int).SetBytes(b[32:64]) - // ignore perStorageAllocation - if !bPerL2Tx.IsUint64() || !bPerL1CalldataUnit.IsUint64() { - err = fmt.Errorf("returned integers are not uint64 (%s, %s)", bPerL2Tx.String(), bPerL1CalldataUnit.String()) - return - } - - perL2TxU64 := bPerL2Tx.Uint64() - perL1CalldataUnitU64 := bPerL1CalldataUnit.Uint64() - if perL2TxU64 > math.MaxUint32 || perL1CalldataUnitU64 > math.MaxUint32 { - err = fmt.Errorf("returned integers are not uint32 (%d, %d)", perL2TxU64, perL1CalldataUnitU64) - return - } - perL2Tx = uint32(perL2TxU64) - perL1CalldataUnit = uint32(perL1CalldataUnitU64) - return -} diff --git a/core/chains/evm/gas/arbitrum_estimator_test.go b/core/chains/evm/gas/arbitrum_estimator_test.go index 3c46b466e87..54d7fc333e3 100644 --- a/core/chains/evm/gas/arbitrum_estimator_test.go +++ b/core/chains/evm/gas/arbitrum_estimator_test.go @@ -19,6 +19,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) @@ -52,9 +53,10 @@ func TestArbitrumEstimator(t *testing.T) { var bumpMin = assets.NewWei(big.NewInt(1)) t.Run("calling GetLegacyGas on unstarted estimator returns error", func(t *testing.T) { - rpcClient := mocks.NewRPCClient(t) - ethClient := mocks.NewETHClient(t) - o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, rpcClient, ethClient) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollups.NewArbitrumL1GasOracle(logger.Test(t), feeEstimatorClient) + + o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, feeEstimatorClient, l1Oracle) _, _, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) assert.EqualError(t, err, "estimator is not started") }) @@ -64,21 +66,22 @@ func TestArbitrumEstimator(t *testing.T) { zeros.Write(common.BigToHash(big.NewInt(0)).Bytes()) zeros.Write(common.BigToHash(big.NewInt(123455)).Bytes()) t.Run("calling GetLegacyGas on started estimator returns estimates", func(t *testing.T) { - rpcClient := mocks.NewRPCClient(t) - ethClient := mocks.NewETHClient(t) - rpcClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollups.NewArbitrumL1GasOracle(logger.Test(t), feeEstimatorClient) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(42) }) - ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + feeEstimatorClient.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, gas.ArbGasInfoAddress, callMsg.To.String()) - assert.Equal(t, gas.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) + assert.Equal(t, rollups.ArbGasInfoAddress, callMsg.To.String()) + assert.Equal(t, rollups.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(zeros.Bytes(), nil) - o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{v: maxGasLimit, bumpPercent: bumpPercent, bumpMin: bumpMin}, rpcClient, ethClient) + o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{v: maxGasLimit, bumpPercent: bumpPercent, bumpMin: bumpMin}, feeEstimatorClient, l1Oracle) servicetest.RunHealthy(t, o) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) require.NoError(t, err) @@ -88,19 +91,20 @@ func TestArbitrumEstimator(t *testing.T) { }) t.Run("gas price is lower than user specified max gas price", func(t *testing.T) { - client := mocks.NewRPCClient(t) - ethClient := mocks.NewETHClient(t) - o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, client, ethClient) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollups.NewArbitrumL1GasOracle(logger.Test(t), feeEstimatorClient) + + o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, feeEstimatorClient, l1Oracle) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(42) }) - ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + feeEstimatorClient.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, gas.ArbGasInfoAddress, callMsg.To.String()) - assert.Equal(t, gas.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) + assert.Equal(t, rollups.ArbGasInfoAddress, callMsg.To.String()) + assert.Equal(t, rollups.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(zeros.Bytes(), nil) @@ -113,19 +117,20 @@ func TestArbitrumEstimator(t *testing.T) { }) t.Run("gas price is lower than global max gas price", func(t *testing.T) { - ethClient := mocks.NewETHClient(t) - client := mocks.NewRPCClient(t) - o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, client, ethClient) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollups.NewArbitrumL1GasOracle(logger.Test(t), feeEstimatorClient) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, feeEstimatorClient, l1Oracle) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(120) }) - ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + feeEstimatorClient.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, gas.ArbGasInfoAddress, callMsg.To.String()) - assert.Equal(t, gas.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) + assert.Equal(t, rollups.ArbGasInfoAddress, callMsg.To.String()) + assert.Equal(t, rollups.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(zeros.Bytes(), nil) @@ -137,24 +142,26 @@ func TestArbitrumEstimator(t *testing.T) { }) t.Run("calling BumpLegacyGas on unstarted arbitrum estimator returns error", func(t *testing.T) { - rpcClient := mocks.NewRPCClient(t) - ethClient := mocks.NewETHClient(t) - o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, rpcClient, ethClient) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollups.NewArbitrumL1GasOracle(logger.Test(t), feeEstimatorClient) + + o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, feeEstimatorClient, l1Oracle) _, _, err := o.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), gasLimit, assets.NewWeiI(10), nil) assert.EqualError(t, err, "estimator is not started") }) t.Run("calling GetLegacyGas on started estimator if initial call failed returns error", func(t *testing.T) { - client := mocks.NewRPCClient(t) - ethClient := mocks.NewETHClient(t) - o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, client, ethClient) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollups.NewArbitrumL1GasOracle(logger.Test(t), feeEstimatorClient) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(pkgerrors.New("kaboom")) - ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, feeEstimatorClient, l1Oracle) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(pkgerrors.New("kaboom")) + feeEstimatorClient.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, gas.ArbGasInfoAddress, callMsg.To.String()) - assert.Equal(t, gas.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) + assert.Equal(t, rollups.ArbGasInfoAddress, callMsg.To.String()) + assert.Equal(t, rollups.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(zeros.Bytes(), nil) @@ -165,17 +172,19 @@ func TestArbitrumEstimator(t *testing.T) { }) t.Run("calling GetDynamicFee always returns error", func(t *testing.T) { - rpcClient := mocks.NewRPCClient(t) - ethClient := mocks.NewETHClient(t) - o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, rpcClient, ethClient) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollups.NewArbitrumL1GasOracle(logger.Test(t), feeEstimatorClient) + + o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, feeEstimatorClient, l1Oracle) _, err := o.GetDynamicFee(testutils.Context(t), maxGasPrice) assert.EqualError(t, err, "dynamic fees are not implemented for this estimator") }) t.Run("calling BumpDynamicFee always returns error", func(t *testing.T) { - rpcClient := mocks.NewRPCClient(t) - ethClient := mocks.NewETHClient(t) - o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, rpcClient, ethClient) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollups.NewArbitrumL1GasOracle(logger.Test(t), feeEstimatorClient) + + o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{}, feeEstimatorClient, l1Oracle) fee := gas.DynamicFee{ FeeCap: assets.NewWeiI(42), TipCap: assets.NewWeiI(5), @@ -185,9 +194,10 @@ func TestArbitrumEstimator(t *testing.T) { }) t.Run("limit computes", func(t *testing.T) { - rpcClient := mocks.NewRPCClient(t) - ethClient := mocks.NewETHClient(t) - rpcClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollups.NewArbitrumL1GasOracle(logger.Test(t), feeEstimatorClient) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(42) }) @@ -201,15 +211,15 @@ func TestArbitrumEstimator(t *testing.T) { b.Write(common.BigToHash(big.NewInt(perL2Tx)).Bytes()) b.Write(common.BigToHash(big.NewInt(perL1Calldata)).Bytes()) b.Write(common.BigToHash(big.NewInt(123455)).Bytes()) - ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + feeEstimatorClient.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, gas.ArbGasInfoAddress, callMsg.To.String()) - assert.Equal(t, gas.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) + assert.Equal(t, rollups.ArbGasInfoAddress, callMsg.To.String()) + assert.Equal(t, rollups.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(b.Bytes(), nil) - o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{v: maxGasLimit, bumpPercent: bumpPercent, bumpMin: bumpMin}, rpcClient, ethClient) + o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{v: maxGasLimit, bumpPercent: bumpPercent, bumpMin: bumpMin}, feeEstimatorClient, l1Oracle) servicetest.RunHealthy(t, o) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) require.NoError(t, err) @@ -220,9 +230,10 @@ func TestArbitrumEstimator(t *testing.T) { }) t.Run("limit exceeds max", func(t *testing.T) { - rpcClient := mocks.NewRPCClient(t) - ethClient := mocks.NewETHClient(t) - rpcClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollups.NewArbitrumL1GasOracle(logger.Test(t), feeEstimatorClient) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(42) }) @@ -235,15 +246,15 @@ func TestArbitrumEstimator(t *testing.T) { b.Write(common.BigToHash(big.NewInt(perL2Tx)).Bytes()) b.Write(common.BigToHash(big.NewInt(perL1Calldata)).Bytes()) b.Write(common.BigToHash(big.NewInt(123455)).Bytes()) - ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + feeEstimatorClient.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, gas.ArbGasInfoAddress, callMsg.To.String()) - assert.Equal(t, gas.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) + assert.Equal(t, rollups.ArbGasInfoAddress, callMsg.To.String()) + assert.Equal(t, rollups.ArbGasInfo_getPricesInArbGas, fmt.Sprintf("%x", callMsg.Data)) assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(b.Bytes(), nil) - o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{v: maxGasLimit, bumpPercent: bumpPercent, bumpMin: bumpMin}, rpcClient, ethClient) + o := gas.NewArbitrumEstimator(logger.Test(t), &arbConfig{v: maxGasLimit, bumpPercent: bumpPercent, bumpMin: bumpMin}, feeEstimatorClient, l1Oracle) servicetest.RunHealthy(t, o) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) require.Error(t, err, "expected error but got (%s, %d)", gasPrice, chainSpecificGasLimit) diff --git a/core/chains/evm/gas/block_history_estimator.go b/core/chains/evm/gas/block_history_estimator.go index 5fb9c5d7173..8b8c626f725 100644 --- a/core/chains/evm/gas/block_history_estimator.go +++ b/core/chains/evm/gas/block_history_estimator.go @@ -24,7 +24,7 @@ import ( commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) @@ -97,7 +97,7 @@ type estimatorGasEstimatorConfig interface { //go:generate mockery --quiet --name Config --output ./mocks/ --case=underscore type BlockHistoryEstimator struct { services.StateMachine - ethClient evmclient.Client + ethClient feeEstimatorClient chainID big.Int config chainConfig eConfig estimatorGasEstimatorConfig @@ -120,13 +120,16 @@ type BlockHistoryEstimator struct { initialFetch atomic.Bool logger logger.SugaredLogger + + l1Oracle rollups.L1Oracle } // NewBlockHistoryEstimator returns a new BlockHistoryEstimator that listens // for new heads and updates the base gas price dynamically based on the // configured percentile of gas prices in that block -func NewBlockHistoryEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg chainConfig, eCfg estimatorGasEstimatorConfig, bhCfg BlockHistoryConfig, chainID big.Int) EvmEstimator { +func NewBlockHistoryEstimator(lggr logger.Logger, ethClient feeEstimatorClient, cfg chainConfig, eCfg estimatorGasEstimatorConfig, bhCfg BlockHistoryConfig, chainID big.Int, l1Oracle rollups.L1Oracle) EvmEstimator { ctx, cancel := context.WithCancel(context.Background()) + b := &BlockHistoryEstimator{ ethClient: ethClient, chainID: chainID, @@ -141,6 +144,7 @@ func NewBlockHistoryEstimator(lggr logger.Logger, ethClient evmclient.Client, cf ctx: ctx, ctxCancel: cancel, logger: logger.Sugared(logger.Named(lggr, "BlockHistoryEstimator")), + l1Oracle: l1Oracle, } return b @@ -230,6 +234,10 @@ func (b *BlockHistoryEstimator) Start(ctx context.Context) error { }) } +func (b *BlockHistoryEstimator) L1Oracle() rollups.L1Oracle { + return b.l1Oracle +} + func (b *BlockHistoryEstimator) Close() error { return b.StopOnce("BlockHistoryEstimator", func() error { b.ctxCancel() diff --git a/core/chains/evm/gas/block_history_estimator_test.go b/core/chains/evm/gas/block_history_estimator_test.go index 5260a22bff3..941b60545ba 100644 --- a/core/chains/evm/gas/block_history_estimator_test.go +++ b/core/chains/evm/gas/block_history_estimator_test.go @@ -24,6 +24,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" + rollupMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" @@ -42,12 +44,12 @@ func newBlockHistoryConfig() *gas.MockBlockHistoryConfig { return c } -func newBlockHistoryEstimatorWithChainID(t *testing.T, c evmclient.Client, cfg gas.Config, gCfg gas.GasEstimatorConfig, bhCfg gas.BlockHistoryConfig, cid big.Int) gas.EvmEstimator { - return gas.NewBlockHistoryEstimator(logger.Test(t), c, cfg, gCfg, bhCfg, cid) +func newBlockHistoryEstimatorWithChainID(t *testing.T, c evmclient.Client, cfg gas.Config, gCfg gas.GasEstimatorConfig, bhCfg gas.BlockHistoryConfig, cid big.Int, l1Oracle rollups.L1Oracle) gas.EvmEstimator { + return gas.NewBlockHistoryEstimator(logger.Test(t), c, cfg, gCfg, bhCfg, cid, l1Oracle) } -func newBlockHistoryEstimator(t *testing.T, c evmclient.Client, cfg gas.Config, gCfg gas.GasEstimatorConfig, bhCfg gas.BlockHistoryConfig) *gas.BlockHistoryEstimator { - iface := newBlockHistoryEstimatorWithChainID(t, c, cfg, gCfg, bhCfg, cltest.FixtureChainID) +func newBlockHistoryEstimator(t *testing.T, c evmclient.Client, cfg gas.Config, gCfg gas.GasEstimatorConfig, bhCfg gas.BlockHistoryConfig, l1Oracle rollups.L1Oracle) *gas.BlockHistoryEstimator { + iface := newBlockHistoryEstimatorWithChainID(t, c, cfg, gCfg, bhCfg, cltest.FixtureChainID, l1Oracle) return gas.BlockHistoryEstimatorFromInterface(iface) } @@ -77,8 +79,9 @@ func TestBlockHistoryEstimator_Start(t *testing.T) { t.Run("loads initial state", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) h := &evmtypes.Head{Hash: utils.NewHash(), Number: 42, BaseFeePerGas: assets.NewWeiI(420)} ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(h, nil) @@ -121,8 +124,9 @@ func TestBlockHistoryEstimator_Start(t *testing.T) { cfg2 := gas.NewMockConfig() ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, ethClient, cfg2, geCfg2, bhCfg2) + bhe := newBlockHistoryEstimator(t, ethClient, cfg2, geCfg2, bhCfg2, l1Oracle) h := &evmtypes.Head{Hash: utils.NewHash(), Number: 42, BaseFeePerGas: assets.NewWeiI(420)} ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(h, nil) @@ -154,8 +158,9 @@ func TestBlockHistoryEstimator_Start(t *testing.T) { t.Run("boots even if initial batch call returns nothing", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) h := &evmtypes.Head{Hash: utils.NewHash(), Number: 42} ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(h, nil) @@ -172,8 +177,9 @@ func TestBlockHistoryEstimator_Start(t *testing.T) { t.Run("starts anyway if fetching latest head fails", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(nil, pkgerrors.New("something exploded")) @@ -193,8 +199,9 @@ func TestBlockHistoryEstimator_Start(t *testing.T) { t.Run("starts anyway if fetching first fetch fails, but errors on estimation", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) h := &evmtypes.Head{Hash: utils.NewHash(), Number: 42, BaseFeePerGas: assets.NewWeiI(420)} ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(h, nil) @@ -216,8 +223,9 @@ func TestBlockHistoryEstimator_Start(t *testing.T) { t.Run("returns error if main context is cancelled", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) h := &evmtypes.Head{Hash: utils.NewHash(), Number: 42, BaseFeePerGas: assets.NewWeiI(420)} ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(h, nil) @@ -232,8 +240,9 @@ func TestBlockHistoryEstimator_Start(t *testing.T) { t.Run("starts anyway even if the fetch context is cancelled due to taking longer than the MaxStartTime", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) h := &evmtypes.Head{Hash: utils.NewHash(), Number: 42, BaseFeePerGas: assets.NewWeiI(420)} ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(h, nil) @@ -261,8 +270,9 @@ func TestBlockHistoryEstimator_OnNewLongestChain(t *testing.T) { bhCfg := newBlockHistoryConfig() geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = false + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg, l1Oracle) assert.Nil(t, gas.GetLatestBaseFee(bhe)) @@ -284,6 +294,8 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { t.Run("with history size of 0, errors", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -295,7 +307,7 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) head := cltest.Head(42) err := bhe.FetchBlocks(testutils.Context(t), head) @@ -305,6 +317,8 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { t.Run("with current block height less than block delay does nothing", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() var blockDelay uint16 = 3 @@ -315,7 +329,7 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) for i := -1; i < 3; i++ { head := cltest.Head(i) @@ -327,6 +341,8 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { t.Run("with error retrieving blocks returns error", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() var blockDelay uint16 = 3 @@ -338,7 +354,7 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) ethClient.On("BatchCallContext", mock.Anything, mock.Anything).Return(pkgerrors.New("something exploded")) @@ -349,6 +365,8 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { t.Run("batch fetches heads and transactions and sets them on the block history estimator instance", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() var blockDelay uint16 @@ -362,7 +380,7 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b41 := evmtypes.Block{ Number: 41, @@ -443,6 +461,8 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { t.Run("does not refetch blocks below EVM.FinalityDepth", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() var blockDelay uint16 @@ -455,7 +475,7 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b0 := evmtypes.Block{ Number: 0, @@ -506,6 +526,8 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { t.Run("replaces blocks on re-org within EVM.FinalityDepth", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() var blockDelay uint16 @@ -518,7 +540,7 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b0 := evmtypes.Block{ Number: 0, @@ -577,6 +599,8 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { t.Run("uses locally cached blocks if they are in the chain", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() var blockDelay uint16 var historySize uint16 = 3 @@ -589,7 +613,7 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b0 := evmtypes.Block{ Number: 0, @@ -634,6 +658,8 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { t.Run("fetches max(BlockHistoryEstimatorCheckInclusionBlocks, BlockHistoryEstimatorBlockHistorySize)", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() var blockDelay uint16 var historySize uint16 = 1 @@ -648,7 +674,7 @@ func TestBlockHistoryEstimator_FetchBlocks(t *testing.T) { geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b42 := evmtypes.Block{ Number: 42, @@ -686,6 +712,8 @@ func TestBlockHistoryEstimator_FetchBlocksAndRecalculate_NoEIP1559(t *testing.T) t.Parallel() ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() bhCfg.BlockDelayF = uint16(0) @@ -698,7 +726,7 @@ func TestBlockHistoryEstimator_FetchBlocksAndRecalculate_NoEIP1559(t *testing.T) geCfg.PriceMaxF = assets.NewWeiI(1000) geCfg.PriceMinF = assets.NewWeiI(0) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b1 := evmtypes.Block{ Number: 1, @@ -744,6 +772,7 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { t.Run("does not crash or set gas price to zero if there are no transactions", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -753,7 +782,7 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = false - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) blocks := []evmtypes.Block{} gas.SetRollingBlockHistory(bhe, blocks) @@ -770,6 +799,8 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { t.Run("sets gas price to EVM.GasEstimator.PriceMax if the calculation would otherwise exceed it", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -780,7 +811,7 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { geCfg.PriceMaxF = maxGasPrice geCfg.PriceMinF = minGasPrice - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) blocks := []evmtypes.Block{ { @@ -805,6 +836,8 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { t.Run("sets gas price to EVM.GasEstimator.PriceMin if the calculation would otherwise fall below it", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -815,7 +848,7 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { geCfg.PriceMaxF = maxGasPrice geCfg.PriceMinF = minGasPrice - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) blocks := []evmtypes.Block{ { @@ -840,6 +873,8 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { t.Run("ignores any transaction with a zero gas limit", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -850,7 +885,7 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { geCfg.PriceMaxF = maxGasPrice geCfg.PriceMinF = minGasPrice - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b1Hash := utils.NewHash() b2Hash := utils.NewHash() @@ -887,6 +922,8 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { t.Run("takes into account zero priced transactions if chain is not Gnosis", func(t *testing.T) { // Because everyone loves free gas! ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -897,7 +934,7 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { geCfg.PriceMaxF = maxGasPrice geCfg.PriceMinF = assets.NewWeiI(0) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b1Hash := utils.NewHash() @@ -920,6 +957,8 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { t.Run("ignores zero priced transactions only on Gnosis", func(t *testing.T) { ethClient := evmtest.NewEthClientMock(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -930,7 +969,7 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { geCfg.PriceMaxF = maxGasPrice geCfg.PriceMinF = assets.NewWeiI(11) // Has to be set as Gnosis will only ignore transactions below this price - ibhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + ibhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) bhe := gas.BlockHistoryEstimatorFromInterface(ibhe) b1Hash := utils.NewHash() @@ -964,6 +1003,8 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { // Seems unlikely we will ever experience gas prices > 9 Petawei on mainnet (praying to the eth Gods 🙏) // But other chains could easily use a different base of account ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -976,7 +1017,7 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { geCfg.PriceMaxF = reasonablyHugeGasPrice geCfg.PriceMinF = assets.NewWeiI(10) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) unreasonablyHugeGasPrice := assets.NewWeiI(1000000).Mul(big.NewInt(math.MaxInt64)) @@ -1011,6 +1052,8 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { t.Run("doesn't panic if gas price is nil (although I'm still unsure how this can happen)", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -1021,7 +1064,7 @@ func TestBlockHistoryEstimator_Recalculate_NoEIP1559(t *testing.T) { geCfg.PriceMaxF = maxGasPrice geCfg.PriceMinF = assets.NewWeiI(100) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b1Hash := utils.NewHash() @@ -1057,6 +1100,7 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { t.Run("does not crash or set gas price to zero if there are no transactions", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -1066,7 +1110,7 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) blocks := []evmtypes.Block{} gas.SetRollingBlockHistory(bhe, blocks) @@ -1095,6 +1139,8 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { t.Run("does not set tip higher than EVM.GasEstimator.PriceMax", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -1106,7 +1152,7 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { geCfg.PriceMinF = assets.NewWeiI(0) geCfg.TipCapMinF = assets.NewWeiI(0) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) blocks := []evmtypes.Block{ { @@ -1133,6 +1179,8 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { t.Run("sets tip cap to EVM.GasEstimator.TipCapMin if the calculation would otherwise fall below it", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -1144,7 +1192,7 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { geCfg.PriceMinF = assets.NewWeiI(0) geCfg.TipCapMinF = assets.NewWeiI(10) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) blocks := []evmtypes.Block{ { @@ -1171,6 +1219,8 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { t.Run("ignores any transaction with a zero gas limit", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -1182,7 +1232,7 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { geCfg.PriceMinF = assets.NewWeiI(0) geCfg.TipCapMinF = assets.NewWeiI(10) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b1Hash := utils.NewHash() b2Hash := utils.NewHash() @@ -1219,6 +1269,8 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { t.Run("respects minimum gas tip cap", func(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -1230,7 +1282,7 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { geCfg.PriceMinF = assets.NewWeiI(0) geCfg.TipCapMinF = assets.NewWeiI(1) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b1Hash := utils.NewHash() @@ -1255,6 +1307,8 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { t.Run("allows to set zero tip cap if minimum allows it", func(t *testing.T) { // Because everyone loves *cheap* gas! ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -1266,7 +1320,7 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { geCfg.PriceMinF = assets.NewWeiI(0) geCfg.TipCapMinF = assets.NewWeiI(0) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) b1Hash := utils.NewHash() @@ -1291,12 +1345,14 @@ func TestBlockHistoryEstimator_Recalculate_EIP1559(t *testing.T) { func TestBlockHistoryEstimator_IsUsable(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) block := evmtypes.Block{ Number: 0, Hash: utils.NewHash(), @@ -1373,13 +1429,15 @@ func TestBlockHistoryEstimator_IsUsable(t *testing.T) { func TestBlockHistoryEstimator_EffectiveTipCap(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = true - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) block := evmtypes.Block{ Number: 0, @@ -1433,13 +1491,15 @@ func TestBlockHistoryEstimator_EffectiveTipCap(t *testing.T) { func TestBlockHistoryEstimator_EffectiveGasPrice(t *testing.T) { ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = false - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) block := evmtypes.Block{ Number: 0, @@ -1772,6 +1832,7 @@ func TestBlockHistoryEstimator_EIP1559Block_Unmarshal(t *testing.T) { func TestBlockHistoryEstimator_GetLegacyGas(t *testing.T) { t.Parallel() + l1Oracle := rollupMocks.NewL1Oracle(t) cfg := gas.NewMockConfig() bhCfg := newBlockHistoryConfig() @@ -1786,7 +1847,7 @@ func TestBlockHistoryEstimator_GetLegacyGas(t *testing.T) { geCfg.PriceMaxF = maxGasPrice geCfg.PriceMinF = assets.NewWeiI(0) - bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg, l1Oracle) blocks := []evmtypes.Block{ { @@ -1830,7 +1891,7 @@ func TestBlockHistoryEstimator_GetLegacyGas(t *testing.T) { geCfg.EIP1559DynamicFeesF = false - bhe = newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg) + bhe = newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg, l1Oracle) gas.SetRollingBlockHistory(bhe, blocks) bhe.Recalculate(cltest.Head(1)) gas.SimulateStart(t, bhe) @@ -1867,7 +1928,9 @@ func TestBlockHistoryEstimator_UseDefaultPriceAsFallback(t *testing.T) { geCfg.PriceDefaultF = assets.NewWeiI(100) ethClient := evmtest.NewEthClientMockWithDefaultChain(t) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + l1Oracle := rollupMocks.NewL1Oracle(t) + + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) h := &evmtypes.Head{Hash: utils.NewHash(), Number: 42, BaseFeePerGas: nil} ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(h, nil) @@ -1918,7 +1981,9 @@ func TestBlockHistoryEstimator_UseDefaultPriceAsFallback(t *testing.T) { geCfg.BumpThresholdF = uint64(1) ethClient := evmtest.NewEthClientMockWithDefaultChain(t) - bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg) + l1Oracle := rollupMocks.NewL1Oracle(t) + + bhe := newBlockHistoryEstimator(t, ethClient, cfg, geCfg, bhCfg, l1Oracle) h := &evmtypes.Head{Hash: utils.NewHash(), Number: 42, BaseFeePerGas: assets.NewWeiI(40)} ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(h, nil) @@ -1967,7 +2032,9 @@ func TestBlockHistoryEstimator_GetDynamicFee(t *testing.T) { geCfg.TipCapMinF = assets.NewWeiI(0) geCfg.PriceMinF = assets.NewWeiI(0) - bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg) + l1Oracle := rollupMocks.NewL1Oracle(t) + + bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg, l1Oracle) blocks := []evmtypes.Block{ { @@ -2065,9 +2132,10 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { lggr, obs := logger.TestObserved(t, zapcore.DebugLevel) geCfg := &gas.MockGasEstimatorConfig{} geCfg.EIP1559DynamicFeesF = false + l1Oracle := rollupMocks.NewL1Oracle(t) bhe := gas.BlockHistoryEstimatorFromInterface( - gas.NewBlockHistoryEstimator(lggr, nil, cfg, geCfg, bhCfg, *testutils.NewRandomEVMChainID()), + gas.NewBlockHistoryEstimator(lggr, nil, cfg, geCfg, bhCfg, *testutils.NewRandomEVMChainID(), l1Oracle), ) attempts := []gas.EvmPriorAttempt{ @@ -2365,8 +2433,9 @@ func TestBlockHistoryEstimator_Bumps(t *testing.T) { geCfg.BumpPercentF = 10 geCfg.BumpMinF = assets.NewWeiI(150) geCfg.PriceMaxF = maxGasPrice + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg, l1Oracle) b1 := evmtypes.Block{ Number: 1, @@ -2394,8 +2463,9 @@ func TestBlockHistoryEstimator_Bumps(t *testing.T) { geCfg.BumpPercentF = 10 geCfg.BumpMinF = assets.NewWeiI(150) geCfg.PriceMaxF = maxGasPrice + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg, l1Oracle) t.Run("ignores nil current gas price", func(t *testing.T) { gasPrice, gasLimit, err := bhe.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), 100000, maxGasPrice, nil) @@ -2475,8 +2545,9 @@ func TestBlockHistoryEstimator_Bumps(t *testing.T) { geCfg.BumpPercentF = 10 geCfg.BumpMinF = assets.NewWeiI(150) geCfg.PriceMaxF = maxGasPrice + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg, l1Oracle) b1 := evmtypes.Block{ BaseFeePerGas: assets.NewWeiI(1), @@ -2508,8 +2579,9 @@ func TestBlockHistoryEstimator_Bumps(t *testing.T) { geCfg.BumpMinF = assets.NewWeiI(150) geCfg.PriceMaxF = maxGasPrice geCfg.TipCapDefaultF = assets.NewWeiI(52) + l1Oracle := rollupMocks.NewL1Oracle(t) - bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg) + bhe := newBlockHistoryEstimator(t, nil, cfg, geCfg, bhCfg, l1Oracle) t.Run("when current tip cap is nil", func(t *testing.T) { originalFee := gas.DynamicFee{FeeCap: assets.NewWeiI(100), TipCap: assets.NewWeiI(25)} diff --git a/core/chains/evm/gas/cmd/arbgas/main.go b/core/chains/evm/gas/cmd/arbgas/main.go deleted file mode 100644 index dc107a50b52..00000000000 --- a/core/chains/evm/gas/cmd/arbgas/main.go +++ /dev/null @@ -1,85 +0,0 @@ -// arbgas takes a single URL argument and prints the result of three GetLegacyGas calls to the Arbitrum gas estimator. -package main - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/rpc" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" -) - -func main() { - if l := len(os.Args); l != 2 { - log.Fatal("Expected one URL argument but got", l-1) - } - url := os.Args[1] - lggr, err := logger.New() - if err != nil { - log.Fatal("Failed to create logger:", err) - } - - ctx := context.Background() - withEstimator(ctx, logger.Sugared(lggr), url, func(e gas.EvmEstimator) { - printGetLegacyGas(ctx, e, make([]byte, 10), 500_000, assets.GWei(1)) - printGetLegacyGas(ctx, e, make([]byte, 10), 500_000, assets.GWei(1), feetypes.OptForceRefetch) - printGetLegacyGas(ctx, e, make([]byte, 10), max, assets.GWei(1)) - }) -} - -func printGetLegacyGas(ctx context.Context, e gas.EvmEstimator, calldata []byte, l2GasLimit uint64, maxGasPrice *assets.Wei, opts ...feetypes.Opt) { - price, limit, err := e.GetLegacyGas(ctx, calldata, l2GasLimit, maxGasPrice, opts...) - if err != nil { - log.Println("failed to get legacy gas:", err) - return - } - fmt.Println("Price:", price) - fmt.Println("Limit:", limit) -} - -const max = 50_000_000 - -func withEstimator(ctx context.Context, lggr logger.SugaredLogger, url string, f func(e gas.EvmEstimator)) { - rc, err := rpc.Dial(url) - if err != nil { - log.Fatal(err) - } - ec := ethclient.NewClient(rc) - e := gas.NewArbitrumEstimator(lggr, &config{max: max}, rc, ec) - ctx, cancel := context.WithCancel(ctx) - defer cancel() - err = e.Start(ctx) - if err != nil { - log.Fatal(err) - } - defer lggr.ErrorIfFn(e.Close, "Error closing ArbitrumEstimator") - - f(e) -} - -var _ gas.ArbConfig = &config{} - -type config struct { - max uint64 - bumpPercent uint16 - bumpMin *assets.Wei -} - -func (c *config) LimitMax() uint64 { - return c.max -} - -func (c *config) BumpPercent() uint16 { - return c.bumpPercent -} - -func (c *config) BumpMin() *assets.Wei { - return c.bumpMin -} diff --git a/core/chains/evm/gas/fixed_price_estimator.go b/core/chains/evm/gas/fixed_price_estimator.go index fc65413d375..f4749b093a1 100644 --- a/core/chains/evm/gas/fixed_price_estimator.go +++ b/core/chains/evm/gas/fixed_price_estimator.go @@ -9,6 +9,7 @@ import ( commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) @@ -18,6 +19,7 @@ type fixedPriceEstimator struct { config fixedPriceEstimatorConfig bhConfig fixedPriceEstimatorBlockHistoryConfig lggr logger.SugaredLogger + l1Oracle rollups.L1Oracle } type bumpConfig interface { LimitMultiplier() float32 @@ -43,8 +45,8 @@ type fixedPriceEstimatorBlockHistoryConfig interface { // NewFixedPriceEstimator returns a new "FixedPrice" estimator which will // always use the config default values for gas prices and limits -func NewFixedPriceEstimator(cfg fixedPriceEstimatorConfig, bhCfg fixedPriceEstimatorBlockHistoryConfig, lggr logger.Logger) EvmEstimator { - return &fixedPriceEstimator{cfg, bhCfg, logger.Sugared(logger.Named(lggr, "FixedPriceEstimator"))} +func NewFixedPriceEstimator(cfg fixedPriceEstimatorConfig, ethClient feeEstimatorClient, bhCfg fixedPriceEstimatorBlockHistoryConfig, lggr logger.Logger, l1Oracle rollups.L1Oracle) EvmEstimator { + return &fixedPriceEstimator{cfg, bhCfg, logger.Sugared(logger.Named(lggr, "FixedPriceEstimator")), l1Oracle} } func (f *fixedPriceEstimator) Start(context.Context) error { @@ -128,6 +130,10 @@ func (f *fixedPriceEstimator) BumpDynamicFee( ) } +func (f *fixedPriceEstimator) L1Oracle() rollups.L1Oracle { + return f.l1Oracle +} + func (f *fixedPriceEstimator) Name() string { return f.lggr.Name() } func (f *fixedPriceEstimator) Ready() error { return nil } func (f *fixedPriceEstimator) HealthReport() map[string]error { return map[string]error{} } diff --git a/core/chains/evm/gas/fixed_price_estimator_test.go b/core/chains/evm/gas/fixed_price_estimator_test.go index c31bd41aeee..9c68f9d2fbc 100644 --- a/core/chains/evm/gas/fixed_price_estimator_test.go +++ b/core/chains/evm/gas/fixed_price_estimator_test.go @@ -9,6 +9,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + rollupMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) @@ -26,7 +27,9 @@ func Test_FixedPriceEstimator(t *testing.T) { t.Run("GetLegacyGas returns EvmGasPriceDefault from config", func(t *testing.T) { config := &gas.MockGasEstimatorConfig{} - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.Test(t)) + l1Oracle := rollupMocks.NewL1Oracle(t) + + f := gas.NewFixedPriceEstimator(config, nil, &blockHistoryConfig{}, logger.Test(t), l1Oracle) config.PriceDefaultF = assets.NewWeiI(42) config.PriceMaxF = maxGasPrice @@ -41,7 +44,9 @@ func Test_FixedPriceEstimator(t *testing.T) { config := &gas.MockGasEstimatorConfig{} config.PriceDefaultF = assets.NewWeiI(42) config.PriceMaxF = assets.NewWeiI(35) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.Test(t)) + l1Oracle := rollupMocks.NewL1Oracle(t) + + f := gas.NewFixedPriceEstimator(config, nil, &blockHistoryConfig{}, logger.Test(t), l1Oracle) gasPrice, gasLimit, err := f.GetLegacyGas(testutils.Context(t), nil, 100000, assets.NewWeiI(30)) require.NoError(t, err) @@ -53,8 +58,9 @@ func Test_FixedPriceEstimator(t *testing.T) { config := &gas.MockGasEstimatorConfig{} config.PriceDefaultF = assets.NewWeiI(42) config.PriceMaxF = assets.NewWeiI(20) + l1Oracle := rollupMocks.NewL1Oracle(t) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.Test(t)) + f := gas.NewFixedPriceEstimator(config, nil, &blockHistoryConfig{}, logger.Test(t), l1Oracle) gasPrice, gasLimit, err := f.GetLegacyGas(testutils.Context(t), nil, 100000, assets.NewWeiI(30)) require.NoError(t, err) assert.Equal(t, 100000, int(gasLimit)) @@ -67,9 +73,10 @@ func Test_FixedPriceEstimator(t *testing.T) { config.PriceMaxF = maxGasPrice config.BumpPercentF = uint16(10) config.BumpMinF = assets.NewWeiI(150) + l1Oracle := rollupMocks.NewL1Oracle(t) lggr := logger.TestSugared(t) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr) + f := gas.NewFixedPriceEstimator(config, nil, &blockHistoryConfig{}, lggr, l1Oracle) gasPrice, gasLimit, err := f.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), 100000, maxGasPrice, nil) require.NoError(t, err) @@ -87,9 +94,10 @@ func Test_FixedPriceEstimator(t *testing.T) { config.TipCapDefaultF = assets.NewWeiI(52) config.FeeCapDefaultF = assets.NewWeiI(100) config.BumpThresholdF = uint64(3) + l1Oracle := rollupMocks.NewL1Oracle(t) lggr := logger.Test(t) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr) + f := gas.NewFixedPriceEstimator(config, nil, &blockHistoryConfig{}, lggr, l1Oracle) fee, err := f.GetDynamicFee(testutils.Context(t), maxGasPrice) require.NoError(t, err) @@ -120,9 +128,10 @@ func Test_FixedPriceEstimator(t *testing.T) { config.TipCapDefaultF = assets.NewWeiI(52) config.BumpMinF = assets.NewWeiI(150) config.BumpPercentF = uint16(10) + l1Oracle := rollupMocks.NewL1Oracle(t) lggr := logger.TestSugared(t) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr) + f := gas.NewFixedPriceEstimator(config, nil, &blockHistoryConfig{}, lggr, l1Oracle) originalFee := gas.DynamicFee{FeeCap: assets.NewWeiI(100), TipCap: assets.NewWeiI(25)} fee, err := f.BumpDynamicFee(testutils.Context(t), originalFee, maxGasPrice, nil) diff --git a/core/chains/evm/gas/mocks/eth_client.go b/core/chains/evm/gas/mocks/eth_client.go deleted file mode 100644 index bb0784f8515..00000000000 --- a/core/chains/evm/gas/mocks/eth_client.go +++ /dev/null @@ -1,61 +0,0 @@ -// Code generated by mockery v2.38.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - big "math/big" - - ethereum "github.com/ethereum/go-ethereum" - - mock "github.com/stretchr/testify/mock" -) - -// ETHClient is an autogenerated mock type for the ethClient type -type ETHClient struct { - mock.Mock -} - -// CallContract provides a mock function with given fields: ctx, msg, blockNumber -func (_m *ETHClient) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - ret := _m.Called(ctx, msg, blockNumber) - - if len(ret) == 0 { - panic("no return value specified for CallContract") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error)); ok { - return rf(ctx, msg, blockNumber) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg, *big.Int) []byte); ok { - r0 = rf(ctx, msg, blockNumber) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg, *big.Int) error); ok { - r1 = rf(ctx, msg, blockNumber) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewETHClient creates a new instance of ETHClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewETHClient(t interface { - mock.TestingT - Cleanup(func()) -}) *ETHClient { - mock := ÐClient{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/core/chains/evm/gas/mocks/evm_estimator.go b/core/chains/evm/gas/mocks/evm_estimator.go index a0b6fa62432..600e43a7c69 100644 --- a/core/chains/evm/gas/mocks/evm_estimator.go +++ b/core/chains/evm/gas/mocks/evm_estimator.go @@ -13,6 +13,8 @@ import ( mock "github.com/stretchr/testify/mock" + rollups "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" + types "github.com/smartcontractkit/chainlink/v2/common/fee/types" ) @@ -196,6 +198,26 @@ func (_m *EvmEstimator) HealthReport() map[string]error { return r0 } +// L1Oracle provides a mock function with given fields: +func (_m *EvmEstimator) L1Oracle() rollups.L1Oracle { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for L1Oracle") + } + + var r0 rollups.L1Oracle + if rf, ok := ret.Get(0).(func() rollups.L1Oracle); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(rollups.L1Oracle) + } + } + + return r0 +} + // Name provides a mock function with given fields: func (_m *EvmEstimator) Name() string { ret := _m.Called() diff --git a/core/chains/evm/gas/mocks/fee_estimator_client.go b/core/chains/evm/gas/mocks/fee_estimator_client.go new file mode 100644 index 00000000000..50eb17d2dac --- /dev/null +++ b/core/chains/evm/gas/mocks/fee_estimator_client.go @@ -0,0 +1,154 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + ethereum "github.com/ethereum/go-ethereum" + + mock "github.com/stretchr/testify/mock" + + rpc "github.com/ethereum/go-ethereum/rpc" + + types "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" +) + +// FeeEstimatorClient is an autogenerated mock type for the feeEstimatorClient type +type FeeEstimatorClient struct { + mock.Mock +} + +// BatchCallContext provides a mock function with given fields: ctx, b +func (_m *FeeEstimatorClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { + ret := _m.Called(ctx, b) + + if len(ret) == 0 { + panic("no return value specified for BatchCallContext") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []rpc.BatchElem) error); ok { + r0 = rf(ctx, b) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CallContext provides a mock function with given fields: ctx, result, method, args +func (_m *FeeEstimatorClient) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { + var _ca []interface{} + _ca = append(_ca, ctx, result, method) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CallContext") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, interface{}, string, ...interface{}) error); ok { + r0 = rf(ctx, result, method, args...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CallContract provides a mock function with given fields: ctx, msg, blockNumber +func (_m *FeeEstimatorClient) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + ret := _m.Called(ctx, msg, blockNumber) + + if len(ret) == 0 { + panic("no return value specified for CallContract") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error)); ok { + return rf(ctx, msg, blockNumber) + } + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg, *big.Int) []byte); ok { + r0 = rf(ctx, msg, blockNumber) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg, *big.Int) error); ok { + r1 = rf(ctx, msg, blockNumber) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ConfiguredChainID provides a mock function with given fields: +func (_m *FeeEstimatorClient) ConfiguredChainID() *big.Int { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ConfiguredChainID") + } + + var r0 *big.Int + if rf, ok := ret.Get(0).(func() *big.Int); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + return r0 +} + +// HeadByNumber provides a mock function with given fields: ctx, n +func (_m *FeeEstimatorClient) HeadByNumber(ctx context.Context, n *big.Int) (*types.Head, error) { + ret := _m.Called(ctx, n) + + if len(ret) == 0 { + panic("no return value specified for HeadByNumber") + } + + var r0 *types.Head + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Head, error)); ok { + return rf(ctx, n) + } + if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *types.Head); ok { + r0 = rf(ctx, n) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Head) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { + r1 = rf(ctx, n) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewFeeEstimatorClient creates a new instance of FeeEstimatorClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFeeEstimatorClient(t interface { + mock.TestingT + Cleanup(func()) +}) *FeeEstimatorClient { + mock := &FeeEstimatorClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/chains/evm/gas/mocks/rpc_client.go b/core/chains/evm/gas/mocks/rpc_client.go deleted file mode 100644 index d1262665f66..00000000000 --- a/core/chains/evm/gas/mocks/rpc_client.go +++ /dev/null @@ -1,49 +0,0 @@ -// Code generated by mockery v2.38.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// RPCClient is an autogenerated mock type for the rpcClient type -type RPCClient struct { - mock.Mock -} - -// CallContext provides a mock function with given fields: ctx, result, method, args -func (_m *RPCClient) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - var _ca []interface{} - _ca = append(_ca, ctx, result, method) - _ca = append(_ca, args...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for CallContext") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, interface{}, string, ...interface{}) error); ok { - r0 = rf(ctx, result, method, args...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// NewRPCClient creates a new instance of RPCClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewRPCClient(t interface { - mock.TestingT - Cleanup(func()) -}) *RPCClient { - mock := &RPCClient{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index 04673d5a622..c50e19373f1 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -5,8 +5,10 @@ import ( "fmt" "math/big" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" pkgerrors "github.com/pkg/errors" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -18,7 +20,6 @@ import ( feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" "github.com/smartcontractkit/chainlink/v2/common/headtracker" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/label" @@ -41,8 +42,17 @@ type EvmFeeEstimator interface { GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (*big.Int, error) } +//go:generate mockery --quiet --name feeEstimatorClient --output ./mocks/ --case=underscore --structname FeeEstimatorClient +type feeEstimatorClient interface { + CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) + BatchCallContext(ctx context.Context, b []rpc.BatchElem) error + CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error + ConfiguredChainID() *big.Int + HeadByNumber(ctx context.Context, n *big.Int) (*evmtypes.Head, error) +} + // NewEstimator returns the estimator for a given config -func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, geCfg evmconfig.GasEstimator) EvmFeeEstimator { +func NewEstimator(lggr logger.Logger, ethClient feeEstimatorClient, cfg Config, geCfg evmconfig.GasEstimator) EvmFeeEstimator { bh := geCfg.BlockHistory() s := geCfg.Mode() lggr.Infow(fmt.Sprintf("Initializing EVM gas estimator in mode: %s", s), @@ -75,27 +85,27 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, ge switch s { case "Arbitrum": newEstimator = func(l logger.Logger) EvmEstimator { - return NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient) + return NewArbitrumEstimator(lggr, geCfg, ethClient, rollups.NewArbitrumL1GasOracle(lggr, ethClient)) } case "BlockHistory": newEstimator = func(l logger.Logger) EvmEstimator { - return NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()) + return NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID(), l1Oracle) } case "FixedPrice": newEstimator = func(l logger.Logger) EvmEstimator { - return NewFixedPriceEstimator(geCfg, bh, lggr) + return NewFixedPriceEstimator(geCfg, ethClient, bh, lggr, l1Oracle) } case "L2Suggested", "SuggestedPrice": newEstimator = func(l logger.Logger) EvmEstimator { - return NewSuggestedPriceEstimator(lggr, ethClient, geCfg) + return NewSuggestedPriceEstimator(lggr, ethClient, geCfg, l1Oracle) } default: lggr.Warnf("GasEstimator: unrecognised mode '%s', falling back to FixedPriceEstimator", s) newEstimator = func(l logger.Logger) EvmEstimator { - return NewFixedPriceEstimator(geCfg, bh, lggr) + return NewFixedPriceEstimator(geCfg, ethClient, bh, lggr, l1Oracle) } } - return NewWrappedEvmEstimator(lggr, newEstimator, df, l1Oracle, geCfg) + return NewEvmFeeEstimator(lggr, newEstimator, df, geCfg) } // DynamicFee encompasses both FeeCap and TipCap for EIP1559 transactions @@ -138,6 +148,8 @@ type EvmEstimator interface { // - be sorted in order from highest price to lowest price // - all be of transaction type 0x2 BumpDynamicFee(ctx context.Context, original DynamicFee, maxGasPriceWei *assets.Wei, attempts []EvmPriorAttempt) (bumped DynamicFee, err error) + + L1Oracle() rollups.L1Oracle } var _ feetypes.Fee = (*EvmFee)(nil) @@ -159,53 +171,53 @@ func (fee EvmFee) ValidDynamic() bool { return fee.DynamicFeeCap != nil && fee.DynamicTipCap != nil } -// WrappedEvmEstimator provides a struct that wraps the EVM specific dynamic and legacy estimators into one estimator that conforms to the generic FeeEstimator -type WrappedEvmEstimator struct { +// evmFeeEstimator provides a struct that wraps the EVM specific dynamic and legacy estimators into one estimator that conforms to the generic FeeEstimator +type evmFeeEstimator struct { services.StateMachine lggr logger.Logger EvmEstimator EIP1559Enabled bool - l1Oracle rollups.L1Oracle geCfg GasEstimatorConfig } -var _ EvmFeeEstimator = (*WrappedEvmEstimator)(nil) +var _ EvmFeeEstimator = (*evmFeeEstimator)(nil) -func NewWrappedEvmEstimator(lggr logger.Logger, newEstimator func(logger.Logger) EvmEstimator, eip1559Enabled bool, l1Oracle rollups.L1Oracle, geCfg GasEstimatorConfig) EvmFeeEstimator { +func NewEvmFeeEstimator(lggr logger.Logger, newEstimator func(logger.Logger) EvmEstimator, eip1559Enabled bool, geCfg GasEstimatorConfig) EvmFeeEstimator { lggr = logger.Named(lggr, "WrappedEvmEstimator") - return &WrappedEvmEstimator{ + return &evmFeeEstimator{ lggr: lggr, EvmEstimator: newEstimator(lggr), EIP1559Enabled: eip1559Enabled, - l1Oracle: l1Oracle, geCfg: geCfg, } } -func (e *WrappedEvmEstimator) Name() string { +func (e *evmFeeEstimator) Name() string { return e.lggr.Name() } -func (e *WrappedEvmEstimator) Start(ctx context.Context) error { +func (e *evmFeeEstimator) Start(ctx context.Context) error { return e.StartOnce(e.Name(), func() error { if err := e.EvmEstimator.Start(ctx); err != nil { return pkgerrors.Wrap(err, "failed to start EVMEstimator") } - if e.l1Oracle != nil { - if err := e.l1Oracle.Start(ctx); err != nil { + l1Oracle := e.L1Oracle() + if l1Oracle != nil { + if err := l1Oracle.Start(ctx); err != nil { return pkgerrors.Wrap(err, "failed to start L1Oracle") } } return nil }) } -func (e *WrappedEvmEstimator) Close() error { +func (e *evmFeeEstimator) Close() error { return e.StopOnce(e.Name(), func() error { var errEVM, errOracle error errEVM = pkgerrors.Wrap(e.EvmEstimator.Close(), "failed to stop EVMEstimator") - if e.l1Oracle != nil { - errOracle = pkgerrors.Wrap(e.l1Oracle.Close(), "failed to stop L1Oracle") + l1Oracle := e.L1Oracle() + if l1Oracle != nil { + errOracle = pkgerrors.Wrap(l1Oracle.Close(), "failed to stop L1Oracle") } if errEVM != nil { @@ -215,12 +227,13 @@ func (e *WrappedEvmEstimator) Close() error { }) } -func (e *WrappedEvmEstimator) Ready() error { +func (e *evmFeeEstimator) Ready() error { var errEVM, errOracle error errEVM = e.EvmEstimator.Ready() - if e.l1Oracle != nil { - errOracle = e.l1Oracle.Ready() + l1Oracle := e.L1Oracle() + if l1Oracle != nil { + errOracle = l1Oracle.Ready() } if errEVM != nil { @@ -229,21 +242,23 @@ func (e *WrappedEvmEstimator) Ready() error { return errOracle } -func (e *WrappedEvmEstimator) HealthReport() map[string]error { +func (e *evmFeeEstimator) HealthReport() map[string]error { report := map[string]error{e.Name(): e.Healthy()} services.CopyHealth(report, e.EvmEstimator.HealthReport()) - if e.l1Oracle != nil { - services.CopyHealth(report, e.l1Oracle.HealthReport()) + + l1Oracle := e.L1Oracle() + if l1Oracle != nil { + services.CopyHealth(report, l1Oracle.HealthReport()) } return report } -func (e *WrappedEvmEstimator) L1Oracle() rollups.L1Oracle { - return e.l1Oracle +func (e *evmFeeEstimator) L1Oracle() rollups.L1Oracle { + return e.EvmEstimator.L1Oracle() } -func (e *WrappedEvmEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error) { +func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error) { // get dynamic fee if e.EIP1559Enabled { var dynamicFee DynamicFee @@ -267,7 +282,7 @@ func (e *WrappedEvmEstimator) GetFee(ctx context.Context, calldata []byte, feeLi return } -func (e *WrappedEvmEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (*big.Int, error) { +func (e *evmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (*big.Int, error) { fees, gasLimit, err := e.GetFee(ctx, calldata, feeLimit, maxFeePrice, opts...) if err != nil { return nil, err @@ -285,7 +300,7 @@ func (e *WrappedEvmEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, return amountWithFees, nil } -func (e *WrappedEvmEstimator) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint64, maxFeePrice *assets.Wei, attempts []EvmPriorAttempt) (bumpedFee EvmFee, chainSpecificFeeLimit uint64, err error) { +func (e *evmFeeEstimator) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint64, maxFeePrice *assets.Wei, attempts []EvmPriorAttempt) (bumpedFee EvmFee, chainSpecificFeeLimit uint64, err error) { // validate only 1 fee type is present if (!originalFee.ValidDynamic() && originalFee.Legacy == nil) || (originalFee.ValidDynamic() && originalFee.Legacy != nil) { err = pkgerrors.New("only one dynamic or legacy fee can be defined") diff --git a/core/chains/evm/gas/models_test.go b/core/chains/evm/gas/models_test.go index ec9542b4040..722beb8021a 100644 --- a/core/chains/evm/gas/models_test.go +++ b/core/chains/evm/gas/models_test.go @@ -10,9 +10,11 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "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" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" rollupMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) @@ -49,14 +51,24 @@ func TestWrappedEvmEstimator(t *testing.T) { // L1Oracle returns the correct L1Oracle interface t.Run("L1Oracle", func(t *testing.T) { lggr := logger.Test(t) + + evmEstimator := mocks.NewEvmEstimator(t) + evmEstimator.On("L1Oracle").Return(nil).Once() + + getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } + // expect nil - estimator := gas.NewWrappedEvmEstimator(lggr, getRootEst, false, nil, nil) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, nil) l1Oracle := estimator.L1Oracle() + assert.Nil(t, l1Oracle) // expect l1Oracle - oracle := rollupMocks.NewL1Oracle(t) - estimator = gas.NewWrappedEvmEstimator(lggr, getRootEst, false, oracle, geCfg) + oracle := rollups.NewL1GasOracle(lggr, nil, config.ChainOptimismBedrock) + // cast oracle to L1Oracle interface + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + + evmEstimator.On("L1Oracle").Return(oracle).Once() l1Oracle = estimator.L1Oracle() assert.Equal(t, oracle, l1Oracle) }) @@ -66,7 +78,7 @@ func TestWrappedEvmEstimator(t *testing.T) { lggr := logger.Test(t) // expect legacy fee data dynamicFees := false - estimator := gas.NewWrappedEvmEstimator(lggr, getRootEst, dynamicFees, nil, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) fee, max, err := estimator.GetFee(ctx, nil, 0, nil) require.NoError(t, err) assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), max) @@ -76,7 +88,7 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect dynamic fee data dynamicFees = true - estimator = gas.NewWrappedEvmEstimator(lggr, getRootEst, dynamicFees, nil, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) fee, max, err = estimator.GetFee(ctx, nil, gasLimit, nil) require.NoError(t, err) assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), max) @@ -89,7 +101,7 @@ func TestWrappedEvmEstimator(t *testing.T) { t.Run("BumpFee", func(t *testing.T) { lggr := logger.Test(t) dynamicFees := false - estimator := gas.NewWrappedEvmEstimator(lggr, getRootEst, dynamicFees, nil, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) // expect legacy fee data fee, max, err := estimator.BumpFee(ctx, gas.EvmFee{Legacy: assets.NewWeiI(0)}, 0, nil, nil) @@ -127,7 +139,7 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect legacy fee data dynamicFees := false - estimator := gas.NewWrappedEvmEstimator(lggr, getRootEst, dynamicFees, nil, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) total, err := estimator.GetMaxCost(ctx, val, nil, gasLimit, nil) require.NoError(t, err) fee := new(big.Int).Mul(legacyFee.ToInt(), big.NewInt(int64(gasLimit))) @@ -136,7 +148,7 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect dynamic fee data dynamicFees = true - estimator = gas.NewWrappedEvmEstimator(lggr, getRootEst, dynamicFees, nil, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) total, err = estimator.GetMaxCost(ctx, val, nil, gasLimit, nil) require.NoError(t, err) fee = new(big.Int).Mul(dynamicFee.FeeCap.ToInt(), big.NewInt(int64(gasLimit))) @@ -147,13 +159,12 @@ func TestWrappedEvmEstimator(t *testing.T) { t.Run("Name", func(t *testing.T) { lggr := logger.Test(t) - oracle := rollupMocks.NewL1Oracle(t) evmEstimator := mocks.NewEvmEstimator(t) evmEstimator.On("Name").Return(mockEvmEstimatorName, nil).Once() - estimator := gas.NewWrappedEvmEstimator(lggr, func(logger.Logger) gas.EvmEstimator { + estimator := gas.NewEvmFeeEstimator(lggr, func(logger.Logger) gas.EvmEstimator { return evmEstimator - }, false, oracle, geCfg) + }, false, geCfg) require.Equal(t, mockEstimatorName, estimator.Name()) require.Equal(t, mockEvmEstimatorName, evmEstimator.Name()) @@ -170,13 +181,17 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle.On("Close").Return(nil).Once() getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } - estimator := gas.NewWrappedEvmEstimator(lggr, getEst, false, nil, geCfg) + evmEstimator.On("L1Oracle", mock.Anything).Return(nil).Twice() + + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) err := estimator.Start(ctx) require.NoError(t, err) err = estimator.Close() require.NoError(t, err) - estimator = gas.NewWrappedEvmEstimator(lggr, getEst, false, oracle, geCfg) + evmEstimator.On("L1Oracle", mock.Anything).Return(oracle).Twice() + + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) err = estimator.Start(ctx) require.NoError(t, err) err = estimator.Close() @@ -188,15 +203,16 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator := mocks.NewEvmEstimator(t) oracle := rollupMocks.NewL1Oracle(t) + evmEstimator.On("L1Oracle").Return(oracle).Twice() evmEstimator.On("Ready").Return(nil).Twice() - oracle.On("Ready").Return(nil).Once() + oracle.On("Ready").Return(nil).Twice() getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } - estimator := gas.NewWrappedEvmEstimator(lggr, getEst, false, nil, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) err := estimator.Ready() require.NoError(t, err) - estimator = gas.NewWrappedEvmEstimator(lggr, getEst, false, oracle, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) err = estimator.Ready() require.NoError(t, err) }) @@ -211,17 +227,21 @@ func TestWrappedEvmEstimator(t *testing.T) { oracleKey := "oracle" oracleError := pkgerrors.New("oracle error") + evmEstimator.On("L1Oracle").Return(nil).Once() evmEstimator.On("HealthReport").Return(map[string]error{evmEstimatorKey: evmEstimatorError}).Twice() + oracle.On("HealthReport").Return(map[string]error{oracleKey: oracleError}).Once() getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } - estimator := gas.NewWrappedEvmEstimator(lggr, getEst, false, nil, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) report := estimator.HealthReport() require.True(t, pkgerrors.Is(report[evmEstimatorKey], evmEstimatorError)) require.Nil(t, report[oracleKey]) require.NotNil(t, report[mockEstimatorName]) - estimator = gas.NewWrappedEvmEstimator(lggr, getEst, false, oracle, geCfg) + evmEstimator.On("L1Oracle").Return(oracle).Once() + + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) report = estimator.HealthReport() require.True(t, pkgerrors.Is(report[evmEstimatorKey], evmEstimatorError)) require.True(t, pkgerrors.Is(report[oracleKey], oracleError)) diff --git a/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go b/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go new file mode 100644 index 00000000000..d0b4c5808ad --- /dev/null +++ b/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go @@ -0,0 +1,300 @@ +package rollups + +import ( + "context" + "fmt" + "math" + "math/big" + "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" + "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/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" +) + +type ArbL1GasOracle interface { + L1Oracle + GetPricesInArbGas() (perL2Tx uint32, perL1CalldataUnit uint32, err error) +} + +// Reads L2-specific precompiles and caches the l1GasPrice set by the L2. +type arbitrumL1Oracle struct { + services.StateMachine + client l1OracleClient + pollPeriod time.Duration + logger logger.SugaredLogger + chainType config.ChainType + + l1GasPriceAddress string + gasPriceMethod string + l1GasPriceMethodAbi abi.ABI + l1GasPriceMu sync.RWMutex + l1GasPrice priceEntry + + l1GasCostAddress string + gasCostMethod string + l1GasCostMethodAbi abi.ABI + + 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 = "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 = "gasEstimateL1Component" + // ArbGasInfo_getPricesInArbGas is the a hex encoded call to: + // `function getPricesInArbGas() external view returns (uint256, uint256, uint256);` + ArbGasInfo_getPricesInArbGas = "02199f34" +) + +func NewArbitrumL1GasOracle(lggr logger.Logger, ethClient l1OracleClient) *arbitrumL1Oracle { + var l1GasPriceAddress, gasPriceMethod, l1GasCostAddress, gasCostMethod string + var l1GasPriceMethodAbi, l1GasCostMethodAbi abi.ABI + var gasPriceErr, gasCostErr error + + l1GasPriceAddress = ArbGasInfoAddress + gasPriceMethod = ArbGasInfo_getL1BaseFeeEstimate + l1GasPriceMethodAbi, gasPriceErr = abi.JSON(strings.NewReader(GetL1BaseFeeEstimateAbiString)) + l1GasCostAddress = ArbNodeInterfaceAddress + gasCostMethod = ArbNodeInterface_gasEstimateL1Component + l1GasCostMethodAbi, gasCostErr = abi.JSON(strings.NewReader(GasEstimateL1ComponentAbiString)) + + if gasPriceErr != nil { + panic("Failed to parse L1 gas price method ABI for chain: arbitrum") + } + if gasCostErr != nil { + panic("Failed to parse L1 gas cost method ABI for chain: arbitrum") + } + + return &arbitrumL1Oracle{ + client: ethClient, + pollPeriod: PollPeriod, + logger: logger.Sugared(logger.Named(lggr, "L1GasOracle(arbitrum)")), + chainType: config.ChainArbitrum, + + l1GasPriceAddress: l1GasPriceAddress, + gasPriceMethod: gasPriceMethod, + l1GasPriceMethodAbi: l1GasPriceMethodAbi, + l1GasCostAddress: l1GasCostAddress, + gasCostMethod: gasCostMethod, + l1GasCostMethodAbi: l1GasCostMethodAbi, + + chInitialised: make(chan struct{}), + chStop: make(chan struct{}), + chDone: make(chan struct{}), + } +} + +func (o *arbitrumL1Oracle) Name() string { + return o.logger.Name() +} + +func (o *arbitrumL1Oracle) Start(ctx context.Context) error { + return o.StartOnce(o.Name(), func() error { + go o.run() + <-o.chInitialised + return nil + }) +} +func (o *arbitrumL1Oracle) Close() error { + return o.StopOnce(o.Name(), func() error { + close(o.chStop) + <-o.chDone + return nil + }) +} + +func (o *arbitrumL1Oracle) HealthReport() map[string]error { + return map[string]error{o.Name(): o.Healthy()} +} + +func (o *arbitrumL1Oracle) 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 *arbitrumL1Oracle) refresh() (t *time.Timer) { + t, err := o.refreshWithError() + if err != nil { + o.SvcErrBuffer.Append(err) + } + return +} + +func (o *arbitrumL1Oracle) refreshWithError() (t *time.Timer, err error) { + t = time.NewTimer(utils.WithJitter(o.pollPeriod)) + + ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + price, err := o.fetchL1GasPrice(ctx) + if err != nil { + return t, err + } + + o.l1GasPriceMu.Lock() + defer o.l1GasPriceMu.Unlock() + o.l1GasPrice = priceEntry{price: assets.NewWei(price), timestamp: time.Now()} + return +} + +func (o *arbitrumL1Oracle) fetchL1GasPrice(ctx context.Context) (price *big.Int, err error) { + var callData, b []byte + precompile := common.HexToAddress(o.l1GasPriceAddress) + callData, err = o.l1GasPriceMethodAbi.Pack(o.gasPriceMethod) + if err != nil { + errMsg := "failed to pack calldata for arbitrum L1 gas price method" + o.logger.Errorf(errMsg) + return nil, fmt.Errorf("%s: %w", errMsg, err) + } + b, err = o.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: callData, + }, nil) + if err != nil { + errMsg := "gas oracle contract call failed" + o.logger.Errorf(errMsg) + return nil, fmt.Errorf("%s: %w", errMsg, err) + } + + if len(b) != 32 { // returns uint256; + errMsg := fmt.Sprintf("return data length (%d) different than expected (%d)", len(b), 32) + o.logger.Criticalf(errMsg) + return nil, fmt.Errorf(errMsg) + } + price = new(big.Int).SetBytes(b) + return price, nil +} + +func (o *arbitrumL1Oracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) { + var timestamp time.Time + ok := o.IfStarted(func() { + o.l1GasPriceMu.RLock() + l1GasPrice = o.l1GasPrice.price + timestamp = o.l1GasPrice.timestamp + o.l1GasPriceMu.RUnlock() + }) + if !ok { + return l1GasPrice, fmt.Errorf("L1GasOracle is not started; cannot estimate gas") + } + if l1GasPrice == nil { + 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 +} + +// 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 *arbitrumL1Oracle) GetGasCost(ctx context.Context, tx *gethtypes.Transaction, blockNum *big.Int) (*assets.Wei, error) { + ctx, cancel := context.WithTimeout(ctx, client.QueryTimeout) + defer cancel() + var callData, b []byte + var err error + + 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) + } + + precompile := common.HexToAddress(o.l1GasCostAddress) + b, err = o.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: callData, + }, 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 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 +} + +// callGetPricesInArbGas calls ArbGasInfo.getPricesInArbGas() on the precompile contract ArbGasInfoAddress. +// +// @return (per L2 tx, per L1 calldata unit, per storage allocation) +// function getPricesInArbGas() external view returns (uint256, uint256, uint256); +// +// https://github.com/OffchainLabs/nitro/blob/f7645453cfc77bf3e3644ea1ac031eff629df325/contracts/src/precompiles/ArbGasInfo.sol#L69 + +func (o *arbitrumL1Oracle) GetPricesInArbGas() (perL2Tx uint32, perL1CalldataUnit uint32, err error) { + ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + precompile := common.HexToAddress(ArbGasInfoAddress) + b, err := o.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: common.Hex2Bytes(ArbGasInfo_getPricesInArbGas), + }, big.NewInt(-1)) + if err != nil { + return 0, 0, err + } + + if len(b) != 3*32 { // returns (uint256, uint256, uint256); + err = fmt.Errorf("return data length (%d) different than expected (%d)", len(b), 3*32) + return + } + bPerL2Tx := new(big.Int).SetBytes(b[:32]) + bPerL1CalldataUnit := new(big.Int).SetBytes(b[32:64]) + // ignore perStorageAllocation + if !bPerL2Tx.IsUint64() || !bPerL1CalldataUnit.IsUint64() { + err = fmt.Errorf("returned integers are not uint64 (%s, %s)", bPerL2Tx.String(), bPerL1CalldataUnit.String()) + return + } + + perL2TxU64 := bPerL2Tx.Uint64() + perL1CalldataUnitU64 := bPerL1CalldataUnit.Uint64() + if perL2TxU64 > math.MaxUint32 || perL1CalldataUnitU64 > math.MaxUint32 { + err = fmt.Errorf("returned integers are not uint32 (%d, %d)", perL2TxU64, perL1CalldataUnitU64) + return + } + perL2Tx = uint32(perL2TxU64) + perL1CalldataUnit = uint32(perL1CalldataUnitU64) + return +} diff --git a/core/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go index ae46071cf0d..05ceb720ab2 100644 --- a/core/chains/evm/gas/rollups/l1_oracle.go +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -5,36 +5,35 @@ 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/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" - "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-common/pkg/logger" - "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" ) -//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) - BatchCallContext(ctx context.Context, b []rpc.BatchElem) error +// L1Oracle provides interface for fetching L1-specific fee components if the chain is an L2. +// For example, on Optimistic Rollups, this oracle can return rollup-specific l1BaseFee +// +//go:generate mockery --quiet --name L1Oracle --output ./mocks/ --case=underscore +type L1Oracle interface { + services.Service + + GasPrice(ctx context.Context) (*assets.Wei, error) + GetGasCost(ctx context.Context, tx *types.Transaction, blockNum *big.Int) (*assets.Wei, error) } -//go:generate mockery --quiet --name daPriceReader --output ./mocks/ --case=underscore --structname DAPriceReader -type daPriceReader interface { - GetDAGasPrice(ctx context.Context) (*big.Int, error) +//go:generate mockery --quiet --name l1OracleClient --output ./mocks/ --case=underscore --structname L1OracleClient +type l1OracleClient interface { + CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) + BatchCallContext(ctx context.Context, b []rpc.BatchElem) error } type priceEntry struct { @@ -42,71 +41,7 @@ type priceEntry struct { timestamp time.Time } -// 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 - gasPriceMethod string - l1GasPriceMethodAbi abi.ABI - l1GasPriceMu sync.RWMutex - l1GasPrice priceEntry - - l1GasCostAddress string - gasCostMethod string - l1GasCostMethodAbi abi.ABI - - priceReader daPriceReader - - 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 = "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 = "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 = "l1BaseFee" - // OPGasOracle_getL1Fee is a hex encoded call to: - // `function getL1Fee(bytes) external view returns (uint256);` - 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 = "l1BaseFee" - // ScrollGasOracle_getL1Fee is a hex encoded call to: - // `function getL1Fee(bytes) external view returns (uint256);` - 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 = "l1BaseFee" - // Interval at which to poll for L1BaseFee. A good starting point is the L1 block time. PollPeriod = 6 * time.Second ) @@ -117,253 +52,18 @@ func IsRollupWithL1Support(chainType config.ChainType) bool { return slices.Contains(supportedChainTypes, chainType) } -func NewL1GasOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType) L1Oracle { - var priceReader daPriceReader - switch chainType { - case config.ChainOptimismBedrock: - priceReader = newOPPriceReader(lggr, ethClient, chainType, OPGasOracleAddress) - case config.ChainKroma: - priceReader = newOPPriceReader(lggr, ethClient, chainType, KromaGasOracleAddress) - default: - priceReader = nil +func NewL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType config.ChainType) L1Oracle { + if !IsRollupWithL1Support(chainType) { + return nil } - return newL1GasOracle(lggr, ethClient, chainType, priceReader) -} - -func newL1GasOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType, priceReader daPriceReader) L1Oracle { - var l1GasPriceAddress, gasPriceMethod, l1GasCostAddress, gasCostMethod string - var l1GasPriceMethodAbi, l1GasCostMethodAbi abi.ABI - var gasPriceErr, gasCostErr error - + var l1Oracle L1Oracle switch chainType { + case config.ChainOptimismBedrock, config.ChainKroma, config.ChainScroll: + l1Oracle = NewOpStackL1GasOracle(lggr, ethClient, chainType) case config.ChainArbitrum: - l1GasPriceAddress = ArbGasInfoAddress - gasPriceMethod = ArbGasInfo_getL1BaseFeeEstimate - l1GasPriceMethodAbi, gasPriceErr = abi.JSON(strings.NewReader(GetL1BaseFeeEstimateAbiString)) - l1GasCostAddress = ArbNodeInterfaceAddress - gasCostMethod = ArbNodeInterface_gasEstimateL1Component - l1GasCostMethodAbi, gasCostErr = abi.JSON(strings.NewReader(GasEstimateL1ComponentAbiString)) - case config.ChainOptimismBedrock: - l1GasPriceAddress = OPGasOracleAddress - gasPriceMethod = OPGasOracle_l1BaseFee - l1GasPriceMethodAbi, gasPriceErr = abi.JSON(strings.NewReader(L1BaseFeeAbiString)) - l1GasCostAddress = OPGasOracleAddress - gasCostMethod = OPGasOracle_getL1Fee - l1GasCostMethodAbi, gasCostErr = abi.JSON(strings.NewReader(GetL1FeeAbiString)) - case config.ChainKroma: - l1GasPriceAddress = KromaGasOracleAddress - gasPriceMethod = KromaGasOracle_l1BaseFee - l1GasPriceMethodAbi, gasPriceErr = abi.JSON(strings.NewReader(L1BaseFeeAbiString)) - l1GasCostAddress = "" - gasCostMethod = "" - case config.ChainScroll: - l1GasPriceAddress = ScrollGasOracleAddress - gasPriceMethod = ScrollGasOracle_l1BaseFee - l1GasPriceMethodAbi, gasPriceErr = abi.JSON(strings.NewReader(L1BaseFeeAbiString)) - l1GasCostAddress = ScrollGasOracleAddress - gasCostMethod = ScrollGasOracle_getL1Fee - l1GasCostMethodAbi, gasCostErr = abi.JSON(strings.NewReader(GetL1FeeAbiString)) + l1Oracle = NewArbitrumL1GasOracle(lggr, ethClient) 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, - gasPriceMethod: gasPriceMethod, - l1GasPriceMethodAbi: l1GasPriceMethodAbi, - l1GasCostAddress: l1GasCostAddress, - gasCostMethod: gasCostMethod, - l1GasCostMethodAbi: l1GasCostMethodAbi, - - priceReader: priceReader, - - 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, 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() - - price, err := o.fetchL1GasPrice(ctx) - if err != nil { - return t, err - } - - o.l1GasPriceMu.Lock() - defer o.l1GasPriceMu.Unlock() - o.l1GasPrice = priceEntry{price: assets.NewWei(price), timestamp: time.Now()} - return -} - -func (o *l1Oracle) fetchL1GasPrice(ctx context.Context) (price *big.Int, err error) { - // if dedicated priceReader exists, use the reader - if o.priceReader != nil { - return o.priceReader.GetDAGasPrice(ctx) - } - - var callData, b []byte - precompile := common.HexToAddress(o.l1GasPriceAddress) - callData, err = o.l1GasPriceMethodAbi.Pack(o.gasPriceMethod) - if err != nil { - errMsg := fmt.Sprintf("failed to pack calldata for %s L1 gas price method", o.chainType) - o.logger.Errorf(errMsg) - return nil, fmt.Errorf("%s: %w", errMsg, err) - } - b, err = o.client.CallContract(ctx, ethereum.CallMsg{ - To: &precompile, - Data: callData, - }, nil) - if err != nil { - errMsg := "gas oracle contract call failed" - o.logger.Errorf(errMsg) - return nil, fmt.Errorf("%s: %w", errMsg, err) - } - - if len(b) != 32 { // returns uint256; - errMsg := fmt.Sprintf("return data length (%d) different than expected (%d)", len(b), 32) - o.logger.Criticalf(errMsg) - return nil, fmt.Errorf(errMsg) - } - price = new(big.Int).SetBytes(b) - return price, nil -} - -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.price - timestamp = o.l1GasPrice.timestamp - o.l1GasPriceMu.RUnlock() - }) - if !ok { - return l1GasPrice, fmt.Errorf("L1GasOracle is not started; cannot estimate gas") - } - if l1GasPrice == nil { - 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 -} - -// 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, client.QueryTimeout) - defer cancel() - var callData, b []byte - var err error - if o.chainType == config.ChainOptimismBedrock || o.chainType == config.ChainScroll { - // Append rlp-encoded tx - var encodedtx []byte - if encodedtx, err = tx.MarshalBinary(); err != nil { - return nil, fmt.Errorf("failed to marshal tx for gas cost estimation: %w", err) - } - 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 { - 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{ - To: &precompile, - Data: callData, - }, 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 + return l1Oracle } diff --git a/core/chains/evm/gas/rollups/l1_oracle_test.go b/core/chains/evm/gas/rollups/l1_oracle_test.go index 4f3b67e2ecf..6efdda6bcff 100644 --- a/core/chains/evm/gas/rollups/l1_oracle_test.go +++ b/core/chains/evm/gas/rollups/l1_oracle_test.go @@ -1,6 +1,7 @@ package rollups import ( + "errors" "math/big" "strings" "testing" @@ -27,9 +28,9 @@ func TestL1Oracle(t *testing.T) { t.Parallel() t.Run("Unsupported ChainType returns nil", func(t *testing.T) { - ethClient := mocks.NewETHClient(t) + ethClient := mocks.NewL1OracleClient(t) - assert.Panicsf(t, func() { NewL1GasOracle(logger.Test(t), ethClient, config.ChainCelo) }, "Received unspported chaintype %s", config.ChainCelo) + assert.Nil(t, NewL1GasOracle(logger.Test(t), ethClient, config.ChainCelo)) }) } @@ -37,7 +38,7 @@ 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) + ethClient := mocks.NewL1OracleClient(t) oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock) @@ -50,7 +51,7 @@ func TestL1Oracle_GasPrice(t *testing.T) { l1GasPriceMethodAbi, err := abi.JSON(strings.NewReader(GetL1BaseFeeEstimateAbiString)) require.NoError(t, err) - ethClient := mocks.NewETHClient(t) + ethClient := mocks.NewL1OracleClient(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) @@ -73,10 +74,34 @@ 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) - priceReader := mocks.NewDAPriceReader(t) - priceReader.On("GetDAGasPrice", mock.Anything).Return(l1BaseFee, nil) + l1GasPriceMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString)) + require.NoError(t, err) + + isEcotoneAbiString, err := abi.JSON(strings.NewReader(OPIsEcotoneAbiString)) + require.NoError(t, err) + + ethClient := mocks.NewL1OracleClient(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) + var payload []byte + payload, err = isEcotoneAbiString.Pack("isEcotone") + require.NoError(t, err) + require.Equal(t, payload, callMsg.Data) + assert.Nil(t, blockNumber) + }).Return(nil, errors.New("not ecotone")).Once() - oracle := newL1GasOracle(logger.Test(t), nil, config.ChainKroma, priceReader) + 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) + var payload []byte + 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) + + oracle := newOpStackL1GasOracle(logger.Test(t), ethClient, config.ChainKroma, KromaGasOracleAddress) servicetest.RunHealthy(t, oracle) gasPrice, err := oracle.GasPrice(testutils.Context(t)) @@ -88,10 +113,34 @@ 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) - priceReader := mocks.NewDAPriceReader(t) - priceReader.On("GetDAGasPrice", mock.Anything).Return(l1BaseFee, nil) + l1GasPriceMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString)) + require.NoError(t, err) + + isEcotoneAbiString, err := abi.JSON(strings.NewReader(OPIsEcotoneAbiString)) + require.NoError(t, err) + + ethClient := mocks.NewL1OracleClient(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) + var payload []byte + payload, err = isEcotoneAbiString.Pack("isEcotone") + require.NoError(t, err) + require.Equal(t, payload, callMsg.Data) + assert.Nil(t, blockNumber) + }).Return(nil, errors.New("not ecotone")).Once() - oracle := newL1GasOracle(logger.Test(t), nil, config.ChainOptimismBedrock, priceReader) + 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) + var payload []byte + 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) + + oracle := newOpStackL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock, OPGasOracleAddress) servicetest.RunHealthy(t, oracle) gasPrice, err := oracle.GasPrice(testutils.Context(t)) @@ -105,7 +154,20 @@ func TestL1Oracle_GasPrice(t *testing.T) { l1GasPriceMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString)) require.NoError(t, err) - ethClient := mocks.NewETHClient(t) + isEcotoneAbiString, err := abi.JSON(strings.NewReader(OPIsEcotoneAbiString)) + require.NoError(t, err) + + ethClient := mocks.NewL1OracleClient(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) + var payload []byte + payload, err = isEcotoneAbiString.Pack("isEcotone") + require.NoError(t, err) + require.Equal(t, payload, callMsg.Data) + assert.Nil(t, blockNumber) + }).Return(nil, errors.New("not ecotone")).Once() + 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) @@ -149,7 +211,7 @@ func TestL1Oracle_GetGasCost(t *testing.T) { result = append(result, baseFee...) result = append(result, l1BaseFeeEstimate...) - ethClient := mocks.NewETHClient(t) + ethClient := mocks.NewL1OracleClient(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) @@ -171,7 +233,7 @@ func TestL1Oracle_GetGasCost(t *testing.T) { blockNum := big.NewInt(1000) tx := types.NewTx(&types.LegacyTx{}) - ethClient := mocks.NewETHClient(t) + ethClient := mocks.NewL1OracleClient(t) oracle := NewL1GasOracle(logger.Test(t), ethClient, config.ChainKroma) _, err := oracle.GetGasCost(testutils.Context(t), tx, blockNum) @@ -195,7 +257,7 @@ func TestL1Oracle_GetGasCost(t *testing.T) { encodedTx, err := tx.MarshalBinary() require.NoError(t, err) - ethClient := mocks.NewETHClient(t) + ethClient := mocks.NewL1OracleClient(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) @@ -230,7 +292,7 @@ func TestL1Oracle_GetGasCost(t *testing.T) { encodedTx, err := tx.MarshalBinary() require.NoError(t, err) - ethClient := mocks.NewETHClient(t) + ethClient := mocks.NewL1OracleClient(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) diff --git a/core/chains/evm/gas/rollups/mocks/da_price_reader.go b/core/chains/evm/gas/rollups/mocks/da_price_reader.go deleted file mode 100644 index 7758f53e436..00000000000 --- a/core/chains/evm/gas/rollups/mocks/da_price_reader.go +++ /dev/null @@ -1,59 +0,0 @@ -// Code generated by mockery v2.38.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - big "math/big" - - mock "github.com/stretchr/testify/mock" -) - -// DAPriceReader is an autogenerated mock type for the daPriceReader type -type DAPriceReader struct { - mock.Mock -} - -// GetDAGasPrice provides a mock function with given fields: ctx -func (_m *DAPriceReader) GetDAGasPrice(ctx context.Context) (*big.Int, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetDAGasPrice") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewDAPriceReader creates a new instance of DAPriceReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewDAPriceReader(t interface { - mock.TestingT - Cleanup(func()) -}) *DAPriceReader { - mock := &DAPriceReader{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/core/chains/evm/gas/rollups/mocks/eth_client.go b/core/chains/evm/gas/rollups/mocks/l1_oracle_client.go similarity index 72% rename from core/chains/evm/gas/rollups/mocks/eth_client.go rename to core/chains/evm/gas/rollups/mocks/l1_oracle_client.go index e5a28f715ad..3995a09513b 100644 --- a/core/chains/evm/gas/rollups/mocks/eth_client.go +++ b/core/chains/evm/gas/rollups/mocks/l1_oracle_client.go @@ -13,13 +13,13 @@ import ( rpc "github.com/ethereum/go-ethereum/rpc" ) -// ETHClient is an autogenerated mock type for the ethClient type -type ETHClient struct { +// L1OracleClient is an autogenerated mock type for the l1OracleClient type +type L1OracleClient struct { mock.Mock } // BatchCallContext provides a mock function with given fields: ctx, b -func (_m *ETHClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { +func (_m *L1OracleClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { ret := _m.Called(ctx, b) if len(ret) == 0 { @@ -37,7 +37,7 @@ func (_m *ETHClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) er } // CallContract provides a mock function with given fields: ctx, msg, blockNumber -func (_m *ETHClient) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { +func (_m *L1OracleClient) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { ret := _m.Called(ctx, msg, blockNumber) if len(ret) == 0 { @@ -66,13 +66,13 @@ func (_m *ETHClient) CallContract(ctx context.Context, msg ethereum.CallMsg, blo return r0, r1 } -// NewETHClient creates a new instance of ETHClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewL1OracleClient creates a new instance of L1OracleClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewETHClient(t interface { +func NewL1OracleClient(t interface { mock.TestingT Cleanup(func()) -}) *ETHClient { - mock := ÐClient{} +}) *L1OracleClient { + mock := &L1OracleClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/core/chains/evm/gas/rollups/models.go b/core/chains/evm/gas/rollups/models.go deleted file mode 100644 index 7aa3d4059dd..00000000000 --- a/core/chains/evm/gas/rollups/models.go +++ /dev/null @@ -1,22 +0,0 @@ -package rollups - -import ( - "context" - "math/big" - - "github.com/ethereum/go-ethereum/core/types" - - "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" -) - -// L1Oracle provides interface for fetching L1-specific fee components if the chain is an L2. -// For example, on Optimistic Rollups, this oracle can return rollup-specific l1BaseFee -// -//go:generate mockery --quiet --name L1Oracle --output ./mocks/ --case=underscore -type L1Oracle interface { - services.Service - - GasPrice(ctx context.Context) (*assets.Wei, error) - GetGasCost(ctx context.Context, tx *types.Transaction, blockNum *big.Int) (*assets.Wei, error) -} diff --git a/core/chains/evm/gas/rollups/op_l1_oracle.go b/core/chains/evm/gas/rollups/op_l1_oracle.go new file mode 100644 index 00000000000..e180777fb61 --- /dev/null +++ b/core/chains/evm/gas/rollups/op_l1_oracle.go @@ -0,0 +1,431 @@ +package rollups + +import ( + "context" + "fmt" + "math/big" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" + + "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/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" +) + +// Reads L2-specific precompiles and caches the l1GasPrice set by the L2. +type OptimismL1Oracle struct { + services.StateMachine + client l1OracleClient + pollPeriod time.Duration + logger logger.SugaredLogger + chainType config.ChainType + + l1OracleAddress string + gasPriceMethod string + l1GasPriceMethodAbi abi.ABI + l1GasPriceMu sync.RWMutex + l1GasPrice priceEntry + + gasCostMethod string + l1GasCostMethodAbi abi.ABI + + chInitialised chan struct{} + chStop services.StopChan + chDone chan struct{} + + isEcotoneMethodAbi abi.ABI + + l1BaseFeeCalldata []byte + isEcotoneCalldata []byte + getL1GasUsedCalldata []byte + getL1FeeCalldata []byte + + isEcotone bool + isEcotoneCheckTs int64 +} + +const ( + // OPStackGasOracle_isEcotone fetches if the OP Stack GasPriceOracle contract has upgraded to Ecotone + OPStackGasOracle_isEcotone = "isEcotone" + // OPStackGasOracle_getL1GasUsed fetches the l1 gas used for given tx bytes + OPStackGasOracle_getL1GasUsed = "getL1GasUsed" + // OPStackGasOracle_isEcotonePollingPeriod is the interval to poll if chain has upgraded to Ecotone + // Set to poll every 4 hours + OPStackGasOracle_isEcotonePollingPeriod = 14400 + // OPStackGasOracleAddress is the address of the precompiled contract that exists on OP stack chain. + // OPStackGasOracle_l1BaseFee fetches the l1 base fee set in the OP Stack GasPriceOracle contract + // OPStackGasOracle_l1BaseFee is a hex encoded call to: + // `function l1BaseFee() external view returns (uint256);` + OPStackGasOracle_l1BaseFee = "l1BaseFee" + // OPStackGasOracle_getL1Fee fetches the l1 fee for given tx bytes + // OPStackGasOracle_getL1Fee is a hex encoded call to: + // `function getL1Fee(bytes) external view returns (uint256);` + OPStackGasOracle_getL1Fee = "getL1Fee" + // This is the case for Optimism and Base. + OPGasOracleAddress = "0x420000000000000000000000000000000000000F" + // GasOracleAddress is the address of the precompiled contract that exists on Kroma chain. + // This is the case for Kroma. + KromaGasOracleAddress = "0x4200000000000000000000000000000000000005" + // ScrollGasOracleAddress is the address of the precompiled contract that exists on Scroll chain. + ScrollGasOracleAddress = "0x5300000000000000000000000000000000000002" +) + +func NewOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType config.ChainType) *OptimismL1Oracle { + var precompileAddress string + switch chainType { + case config.ChainOptimismBedrock: + precompileAddress = OPGasOracleAddress + case config.ChainKroma: + precompileAddress = KromaGasOracleAddress + case config.ChainScroll: + precompileAddress = ScrollGasOracleAddress + default: + panic(fmt.Sprintf("Received unspported chaintype %s", chainType)) + } + return newOpStackL1GasOracle(lggr, ethClient, chainType, precompileAddress) +} + +func newOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType config.ChainType, precompileAddress string) *OptimismL1Oracle { + var l1OracleAddress, gasPriceMethod, gasCostMethod string + var l1GasPriceMethodAbi, l1GasCostMethodAbi abi.ABI + var gasPriceErr, gasCostErr error + + l1OracleAddress = precompileAddress + gasPriceMethod = OPStackGasOracle_l1BaseFee + l1GasPriceMethodAbi, gasPriceErr = abi.JSON(strings.NewReader(L1BaseFeeAbiString)) + gasCostMethod = OPStackGasOracle_getL1Fee + l1GasCostMethodAbi, gasCostErr = abi.JSON(strings.NewReader(GetL1FeeAbiString)) + + 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)) + } + + // encode calldata for each method; these calldata will remain the same for each call, we can encode them just once + l1BaseFeeMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString)) + if err != nil { + panic(fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for chain: %s; %w", OPStackGasOracle_l1BaseFee, chainType, err)) + } + l1BaseFeeCalldata, err := l1BaseFeeMethodAbi.Pack(OPStackGasOracle_l1BaseFee) + if err != nil { + panic(fmt.Errorf("failed to parse GasPriceOracle %s() calldata for chain: %s; %w", OPStackGasOracle_l1BaseFee, chainType, err)) + } + + isEcotoneMethodAbi, err := abi.JSON(strings.NewReader(OPIsEcotoneAbiString)) + if err != nil { + panic(fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for chain: %s; %w", OPStackGasOracle_isEcotone, chainType, err)) + } + isEcotoneCalldata, err := isEcotoneMethodAbi.Pack(OPStackGasOracle_isEcotone) + if err != nil { + panic(fmt.Errorf("failed to parse GasPriceOracle %s() calldata for chain: %s; %w", OPStackGasOracle_isEcotone, chainType, err)) + } + + getL1GasUsedMethodAbi, err := abi.JSON(strings.NewReader(OPGetL1GasUsedAbiString)) + if err != nil { + panic(fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for chain: %s; %w", OPStackGasOracle_getL1GasUsed, chainType, err)) + } + getL1GasUsedCalldata, err := getL1GasUsedMethodAbi.Pack(OPStackGasOracle_getL1GasUsed, []byte{0x1}) + if err != nil { + panic(fmt.Errorf("failed to parse GasPriceOracle %s() calldata for chain: %s; %w", OPStackGasOracle_getL1GasUsed, chainType, err)) + } + + getL1FeeMethodAbi, err := abi.JSON(strings.NewReader(GetL1FeeAbiString)) + if err != nil { + panic(fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for chain: %s; %w", OPStackGasOracle_getL1Fee, chainType, err)) + } + getL1FeeCalldata, err := getL1FeeMethodAbi.Pack(OPStackGasOracle_getL1Fee, []byte{0x1}) + if err != nil { + panic(fmt.Errorf("failed to parse GasPriceOracle %s() calldata for chain: %s; %w", OPStackGasOracle_getL1Fee, chainType, err)) + } + + return &OptimismL1Oracle{ + client: ethClient, + pollPeriod: PollPeriod, + logger: logger.Sugared(logger.Named(lggr, "L1GasOracle(optimismBedrock)")), + chainType: chainType, + + l1OracleAddress: l1OracleAddress, + gasPriceMethod: gasPriceMethod, + l1GasPriceMethodAbi: l1GasPriceMethodAbi, + gasCostMethod: gasCostMethod, + l1GasCostMethodAbi: l1GasCostMethodAbi, + + chInitialised: make(chan struct{}), + chStop: make(chan struct{}), + chDone: make(chan struct{}), + + isEcotoneMethodAbi: isEcotoneMethodAbi, + + l1BaseFeeCalldata: l1BaseFeeCalldata, + isEcotoneCalldata: isEcotoneCalldata, + getL1GasUsedCalldata: getL1GasUsedCalldata, + getL1FeeCalldata: getL1FeeCalldata, + + isEcotone: false, + isEcotoneCheckTs: 0, + } +} + +func (o *OptimismL1Oracle) Name() string { + return o.logger.Name() +} + +func (o *OptimismL1Oracle) Start(ctx context.Context) error { + return o.StartOnce(o.Name(), func() error { + go o.run() + <-o.chInitialised + return nil + }) +} +func (o *OptimismL1Oracle) Close() error { + return o.StopOnce(o.Name(), func() error { + close(o.chStop) + <-o.chDone + return nil + }) +} + +func (o *OptimismL1Oracle) HealthReport() map[string]error { + return map[string]error{o.Name(): o.Healthy()} +} + +func (o *OptimismL1Oracle) 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 *OptimismL1Oracle) refresh() (t *time.Timer) { + t, err := o.refreshWithError() + if err != nil { + o.SvcErrBuffer.Append(err) + } + return +} + +func (o *OptimismL1Oracle) refreshWithError() (t *time.Timer, err error) { + t = time.NewTimer(utils.WithJitter(o.pollPeriod)) + + ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + price, err := o.GetDAGasPrice(ctx) + if err != nil { + return t, err + } + + o.l1GasPriceMu.Lock() + defer o.l1GasPriceMu.Unlock() + o.l1GasPrice = priceEntry{price: assets.NewWei(price), timestamp: time.Now()} + return +} + +func (o *OptimismL1Oracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) { + var timestamp time.Time + ok := o.IfStarted(func() { + o.l1GasPriceMu.RLock() + l1GasPrice = o.l1GasPrice.price + timestamp = o.l1GasPrice.timestamp + o.l1GasPriceMu.RUnlock() + }) + if !ok { + return l1GasPrice, fmt.Errorf("L1GasOracle is not started; cannot estimate gas") + } + if l1GasPrice == nil { + 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 +} + +// 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 *OptimismL1Oracle) GetGasCost(ctx context.Context, tx *gethtypes.Transaction, blockNum *big.Int) (*assets.Wei, error) { + ctx, cancel := context.WithTimeout(ctx, client.QueryTimeout) + defer cancel() + var callData, b []byte + var err error + if o.chainType == config.ChainKroma { + return nil, fmt.Errorf("L1 gas cost not supported for this chain: %s", o.chainType) + } + // Append rlp-encoded tx + var encodedtx []byte + if encodedtx, err = tx.MarshalBinary(); err != nil { + return nil, fmt.Errorf("failed to marshal tx for gas cost estimation: %w", err) + } + 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) + } + + precompile := common.HexToAddress(o.l1OracleAddress) + b, err = o.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: callData, + }, 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 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) + + return assets.NewWei(l1GasCost), nil +} + +func (o *OptimismL1Oracle) GetDAGasPrice(ctx context.Context) (*big.Int, error) { + isEcotone, err := o.checkIsEcotone(ctx) + if err != nil { + return nil, err + } + + o.logger.Infof("Chain isEcotone result: %t", isEcotone) + + if isEcotone { + return o.getEcotoneGasPrice(ctx) + } + + return o.getV1GasPrice(ctx) +} + +func (o *OptimismL1Oracle) checkIsEcotone(ctx context.Context) (bool, error) { + // if chain is already Ecotone, NOOP + if o.isEcotone { + return true, nil + } + // if time since last check has not exceeded polling period, NOOP + if time.Now().Unix()-o.isEcotoneCheckTs < OPStackGasOracle_isEcotonePollingPeriod { + return false, nil + } + o.isEcotoneCheckTs = time.Now().Unix() + + l1OracleAddress := common.HexToAddress(o.l1OracleAddress) + // confirmed with OP team that isEcotone() is the canonical way to check if the chain has upgraded + b, err := o.client.CallContract(ctx, ethereum.CallMsg{ + To: &l1OracleAddress, + Data: o.isEcotoneCalldata, + }, nil) + + // if the chain has not upgraded to Ecotone, the isEcotone call will revert, this would be expected + if err != nil { + o.logger.Infof("isEcotone() call failed, this can happen if chain has not upgraded: %w", err) + return false, nil + } + + res, err := o.isEcotoneMethodAbi.Unpack(OPStackGasOracle_isEcotone, b) + if err != nil { + return false, fmt.Errorf("failed to unpack isEcotone() return data: %w", err) + } + o.isEcotone = res[0].(bool) + return o.isEcotone, nil +} + +func (o *OptimismL1Oracle) getV1GasPrice(ctx context.Context) (*big.Int, error) { + l1OracleAddress := common.HexToAddress(o.l1OracleAddress) + b, err := o.client.CallContract(ctx, ethereum.CallMsg{ + To: &l1OracleAddress, + Data: o.l1BaseFeeCalldata, + }, nil) + if err != nil { + return nil, fmt.Errorf("l1BaseFee() call failed: %w", err) + } + + if len(b) != 32 { + return nil, fmt.Errorf("l1BaseFee() return data length (%d) different than expected (%d)", len(b), 32) + } + return new(big.Int).SetBytes(b), nil +} + +func (o *OptimismL1Oracle) getEcotoneGasPrice(ctx context.Context) (*big.Int, error) { + rpcBatchCalls := []rpc.BatchElem{ + { + Method: "eth_call", + Args: []any{ + map[string]interface{}{ + "from": common.Address{}, + "to": o.l1OracleAddress, + "data": hexutil.Bytes(o.getL1GasUsedCalldata), + }, + "latest", + }, + Result: new(string), + }, + { + Method: "eth_call", + Args: []any{ + map[string]interface{}{ + "from": common.Address{}, + "to": o.l1OracleAddress, + "data": hexutil.Bytes(o.getL1FeeCalldata), + }, + "latest", + }, + Result: new(string), + }, + } + + err := o.client.BatchCallContext(ctx, rpcBatchCalls) + if err != nil { + return nil, fmt.Errorf("getEcotoneGasPrice batch call failed: %w", err) + } + if rpcBatchCalls[0].Error != nil { + return nil, fmt.Errorf("%s call failed in a batch: %w", OPStackGasOracle_getL1GasUsed, err) + } + if rpcBatchCalls[1].Error != nil { + return nil, fmt.Errorf("%s call failed in a batch: %w", OPStackGasOracle_getL1Fee, err) + } + + l1GasUsedResult := *(rpcBatchCalls[0].Result.(*string)) + l1FeeResult := *(rpcBatchCalls[1].Result.(*string)) + + l1GasUsedBytes, err := hexutil.Decode(l1GasUsedResult) + if err != nil { + return nil, fmt.Errorf("failed to decode %s rpc result: %w", OPStackGasOracle_getL1GasUsed, err) + } + l1FeeBytes, err := hexutil.Decode(l1FeeResult) + if err != nil { + return nil, fmt.Errorf("failed to decode %s rpc result: %w", OPStackGasOracle_getL1Fee, err) + } + + l1GasUsed := new(big.Int).SetBytes(l1GasUsedBytes) + l1Fee := new(big.Int).SetBytes(l1FeeBytes) + + // for the same tx byte, l1Fee / l1GasUsed will give the l1 gas price + // note this price is per l1 gas, not l1 data byte + return new(big.Int).Div(l1Fee, l1GasUsed), nil +} diff --git a/core/chains/evm/gas/rollups/op_price_reader_test.go b/core/chains/evm/gas/rollups/op_l1_oracle_test.go similarity index 90% rename from core/chains/evm/gas/rollups/op_price_reader_test.go rename to core/chains/evm/gas/rollups/op_l1_oracle_test.go index dad12a16366..36e8700faff 100644 --- a/core/chains/evm/gas/rollups/op_price_reader_test.go +++ b/core/chains/evm/gas/rollups/op_l1_oracle_test.go @@ -60,7 +60,7 @@ func TestDAPriceReader_ReadV1GasPrice(t *testing.T) { isEcotoneCalldata, err := isEcotoneMethodAbi.Pack(OPStackGasOracle_isEcotone) require.NoError(t, err) - ethClient := mocks.NewETHClient(t) + ethClient := mocks.NewL1OracleClient(t) call := 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) @@ -87,7 +87,7 @@ func TestDAPriceReader_ReadV1GasPrice(t *testing.T) { }).Return(common.BigToHash(l1BaseFee).Bytes(), nil).Once() } - oracle := newOPPriceReader(logger.Test(t), ethClient, config.ChainOptimismBedrock, oracleAddress) + oracle := newOpStackL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock, oracleAddress) gasPrice, err := oracle.GetDAGasPrice(testutils.Context(t)) if tc.returnBadData { @@ -100,13 +100,13 @@ func TestDAPriceReader_ReadV1GasPrice(t *testing.T) { } } -func setupIsEcotone(t *testing.T, oracleAddress string) *mocks.ETHClient { +func setupIsEcotone(t *testing.T, oracleAddress string) *mocks.L1OracleClient { isEcotoneMethodAbi, err := abi.JSON(strings.NewReader(OPIsEcotoneAbiString)) require.NoError(t, err) isEcotoneCalldata, err := isEcotoneMethodAbi.Pack(OPStackGasOracle_isEcotone) require.NoError(t, err) - ethClient := mocks.NewETHClient(t) + ethClient := mocks.NewL1OracleClient(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) @@ -142,7 +142,7 @@ func TestDAPriceReader_ReadEcotoneGasPrice(t *testing.T) { for _, rE := range rpcElements { require.Equal(t, "eth_call", rE.Method) - require.Equal(t, oracleAddress, rE.Args[0].(map[string]interface{})["to"].(common.Address).String()) + require.Equal(t, oracleAddress, rE.Args[0].(map[string]interface{})["to"]) require.Equal(t, "latest", rE.Args[1]) } @@ -155,7 +155,7 @@ func TestDAPriceReader_ReadEcotoneGasPrice(t *testing.T) { rpcElements[1].Result = &res2 }).Return(nil).Once() - oracle := newOPPriceReader(logger.Test(t), ethClient, config.ChainOptimismBedrock, oracleAddress) + oracle := newOpStackL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock, oracleAddress) gasPrice, err := oracle.GetDAGasPrice(testutils.Context(t)) require.NoError(t, err) assert.Equal(t, l1BaseFee, gasPrice) @@ -170,7 +170,7 @@ func TestDAPriceReader_ReadEcotoneGasPrice(t *testing.T) { rpcElements[1].Result = &badData }).Return(nil).Once() - oracle := newOPPriceReader(logger.Test(t), ethClient, config.ChainOptimismBedrock, oracleAddress) + oracle := newOpStackL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock, oracleAddress) _, err := oracle.GetDAGasPrice(testutils.Context(t)) assert.Error(t, err) }) @@ -179,7 +179,7 @@ func TestDAPriceReader_ReadEcotoneGasPrice(t *testing.T) { ethClient := setupIsEcotone(t, oracleAddress) ethClient.On("BatchCallContext", mock.Anything, mock.IsType([]rpc.BatchElem{})).Return(fmt.Errorf("revert")).Once() - oracle := newOPPriceReader(logger.Test(t), ethClient, config.ChainOptimismBedrock, oracleAddress) + oracle := newOpStackL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock, oracleAddress) _, err := oracle.GetDAGasPrice(testutils.Context(t)) assert.Error(t, err) }) @@ -193,7 +193,7 @@ func TestDAPriceReader_ReadEcotoneGasPrice(t *testing.T) { rpcElements[1].Error = fmt.Errorf("revert") }).Return(nil).Once() - oracle := newOPPriceReader(logger.Test(t), ethClient, config.ChainOptimismBedrock, oracleAddress) + oracle := newOpStackL1GasOracle(logger.Test(t), ethClient, config.ChainOptimismBedrock, oracleAddress) _, err := oracle.GetDAGasPrice(testutils.Context(t)) assert.Error(t, err) }) diff --git a/core/chains/evm/gas/rollups/op_price_reader.go b/core/chains/evm/gas/rollups/op_price_reader.go deleted file mode 100644 index 2d3d668ad8b..00000000000 --- a/core/chains/evm/gas/rollups/op_price_reader.go +++ /dev/null @@ -1,228 +0,0 @@ -package rollups - -import ( - "context" - "fmt" - "math/big" - "strings" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/rpc" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - - "github.com/smartcontractkit/chainlink/v2/common/config" -) - -const ( - // OPStackGasOracle_l1BaseFee fetches the l1 base fee set in the OP Stack GasPriceOracle contract - OPStackGasOracle_l1BaseFee = "l1BaseFee" - - // OPStackGasOracle_isEcotone fetches if the OP Stack GasPriceOracle contract has upgraded to Ecotone - OPStackGasOracle_isEcotone = "isEcotone" - - // OPStackGasOracle_getL1GasUsed fetches the l1 gas used for given tx bytes - OPStackGasOracle_getL1GasUsed = "getL1GasUsed" - - // OPStackGasOracle_getL1Fee fetches the l1 fee for given tx bytes - OPStackGasOracle_getL1Fee = "getL1Fee" - - // OPStackGasOracle_isEcotonePollingPeriod is the interval to poll if chain has upgraded to Ecotone - // Set to poll every 4 hours - OPStackGasOracle_isEcotonePollingPeriod = 14400 -) - -type opStackGasPriceReader struct { - client ethClient - logger logger.SugaredLogger - - oracleAddress common.Address - isEcotoneMethodAbi abi.ABI - - l1BaseFeeCalldata []byte - isEcotoneCalldata []byte - getL1GasUsedCalldata []byte - getL1FeeCalldata []byte - - isEcotone bool - isEcotoneCheckTs int64 -} - -func newOPPriceReader(lggr logger.Logger, ethClient ethClient, chainType config.ChainType, oracleAddress string) daPriceReader { - // encode calldata for each method; these calldata will remain the same for each call, we can encode them just once - l1BaseFeeMethodAbi, err := abi.JSON(strings.NewReader(L1BaseFeeAbiString)) - if err != nil { - panic(fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for chain: %s; %w", OPStackGasOracle_l1BaseFee, chainType, err)) - } - l1BaseFeeCalldata, err := l1BaseFeeMethodAbi.Pack(OPStackGasOracle_l1BaseFee) - if err != nil { - panic(fmt.Errorf("failed to parse GasPriceOracle %s() calldata for chain: %s; %w", OPStackGasOracle_l1BaseFee, chainType, err)) - } - - isEcotoneMethodAbi, err := abi.JSON(strings.NewReader(OPIsEcotoneAbiString)) - if err != nil { - panic(fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for chain: %s; %w", OPStackGasOracle_isEcotone, chainType, err)) - } - isEcotoneCalldata, err := isEcotoneMethodAbi.Pack(OPStackGasOracle_isEcotone) - if err != nil { - panic(fmt.Errorf("failed to parse GasPriceOracle %s() calldata for chain: %s; %w", OPStackGasOracle_isEcotone, chainType, err)) - } - - getL1GasUsedMethodAbi, err := abi.JSON(strings.NewReader(OPGetL1GasUsedAbiString)) - if err != nil { - panic(fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for chain: %s; %w", OPStackGasOracle_getL1GasUsed, chainType, err)) - } - getL1GasUsedCalldata, err := getL1GasUsedMethodAbi.Pack(OPStackGasOracle_getL1GasUsed, []byte{0x1}) - if err != nil { - panic(fmt.Errorf("failed to parse GasPriceOracle %s() calldata for chain: %s; %w", OPStackGasOracle_getL1GasUsed, chainType, err)) - } - - getL1FeeMethodAbi, err := abi.JSON(strings.NewReader(GetL1FeeAbiString)) - if err != nil { - panic(fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for chain: %s; %w", OPStackGasOracle_getL1Fee, chainType, err)) - } - getL1FeeCalldata, err := getL1FeeMethodAbi.Pack(OPStackGasOracle_getL1Fee, []byte{0x1}) - if err != nil { - panic(fmt.Errorf("failed to parse GasPriceOracle %s() calldata for chain: %s; %w", OPStackGasOracle_getL1Fee, chainType, err)) - } - - return &opStackGasPriceReader{ - client: ethClient, - logger: logger.Sugared(logger.Named(lggr, fmt.Sprintf("OPStackGasOracle(%s)", chainType))), - - oracleAddress: common.HexToAddress(oracleAddress), - isEcotoneMethodAbi: isEcotoneMethodAbi, - - l1BaseFeeCalldata: l1BaseFeeCalldata, - isEcotoneCalldata: isEcotoneCalldata, - getL1GasUsedCalldata: getL1GasUsedCalldata, - getL1FeeCalldata: getL1FeeCalldata, - - isEcotone: false, - isEcotoneCheckTs: 0, - } -} - -func (o *opStackGasPriceReader) GetDAGasPrice(ctx context.Context) (*big.Int, error) { - isEcotone, err := o.checkIsEcotone(ctx) - if err != nil { - return nil, err - } - - o.logger.Infof("Chain isEcotone result: %t", isEcotone) - - if isEcotone { - return o.getEcotoneGasPrice(ctx) - } - - return o.getV1GasPrice(ctx) -} - -func (o *opStackGasPriceReader) checkIsEcotone(ctx context.Context) (bool, error) { - // if chain is already Ecotone, NOOP - if o.isEcotone { - return true, nil - } - // if time since last check has not exceeded polling period, NOOP - if time.Now().Unix()-o.isEcotoneCheckTs < OPStackGasOracle_isEcotonePollingPeriod { - return false, nil - } - o.isEcotoneCheckTs = time.Now().Unix() - - // confirmed with OP team that isEcotone() is the canonical way to check if the chain has upgraded - b, err := o.client.CallContract(ctx, ethereum.CallMsg{ - To: &o.oracleAddress, - Data: o.isEcotoneCalldata, - }, nil) - - // if the chain has not upgraded to Ecotone, the isEcotone call will revert, this would be expected - if err != nil { - o.logger.Infof("isEcotone() call failed, this can happen if chain has not upgraded: %w", err) - return false, nil - } - - res, err := o.isEcotoneMethodAbi.Unpack(OPStackGasOracle_isEcotone, b) - if err != nil { - return false, fmt.Errorf("failed to unpack isEcotone() return data: %w", err) - } - o.isEcotone = res[0].(bool) - return o.isEcotone, nil -} - -func (o *opStackGasPriceReader) getV1GasPrice(ctx context.Context) (*big.Int, error) { - b, err := o.client.CallContract(ctx, ethereum.CallMsg{ - To: &o.oracleAddress, - Data: o.l1BaseFeeCalldata, - }, nil) - if err != nil { - return nil, fmt.Errorf("l1BaseFee() call failed: %w", err) - } - - if len(b) != 32 { - return nil, fmt.Errorf("l1BaseFee() return data length (%d) different than expected (%d)", len(b), 32) - } - return new(big.Int).SetBytes(b), nil -} - -func (o *opStackGasPriceReader) getEcotoneGasPrice(ctx context.Context) (*big.Int, error) { - rpcBatchCalls := []rpc.BatchElem{ - { - Method: "eth_call", - Args: []any{ - map[string]interface{}{ - "from": common.Address{}, - "to": o.oracleAddress, - "data": hexutil.Bytes(o.getL1GasUsedCalldata), - }, - "latest", - }, - Result: new(string), - }, - { - Method: "eth_call", - Args: []any{ - map[string]interface{}{ - "from": common.Address{}, - "to": o.oracleAddress, - "data": hexutil.Bytes(o.getL1FeeCalldata), - }, - "latest", - }, - Result: new(string), - }, - } - - err := o.client.BatchCallContext(ctx, rpcBatchCalls) - if err != nil { - return nil, fmt.Errorf("getEcotoneGasPrice batch call failed: %w", err) - } - if rpcBatchCalls[0].Error != nil { - return nil, fmt.Errorf("%s call failed in a batch: %w", OPStackGasOracle_getL1GasUsed, err) - } - if rpcBatchCalls[1].Error != nil { - return nil, fmt.Errorf("%s call failed in a batch: %w", OPStackGasOracle_getL1Fee, err) - } - - l1GasUsedResult := *(rpcBatchCalls[0].Result.(*string)) - l1FeeResult := *(rpcBatchCalls[1].Result.(*string)) - - l1GasUsedBytes, err := hexutil.Decode(l1GasUsedResult) - if err != nil { - return nil, fmt.Errorf("failed to decode %s rpc result: %w", OPStackGasOracle_getL1GasUsed, err) - } - l1FeeBytes, err := hexutil.Decode(l1FeeResult) - if err != nil { - return nil, fmt.Errorf("failed to decode %s rpc result: %w", OPStackGasOracle_getL1Fee, err) - } - - l1GasUsed := new(big.Int).SetBytes(l1GasUsedBytes) - l1Fee := new(big.Int).SetBytes(l1FeeBytes) - - // for the same tx byte, l1Fee / l1GasUsed will give the l1 gas price - // note this price is per l1 gas, not l1 data byte - return new(big.Int).Div(l1Fee, l1GasUsed), nil -} diff --git a/core/chains/evm/gas/suggested_price_estimator.go b/core/chains/evm/gas/suggested_price_estimator.go index edc1b0f92fa..e947e9109d1 100644 --- a/core/chains/evm/gas/suggested_price_estimator.go +++ b/core/chains/evm/gas/suggested_price_estimator.go @@ -19,6 +19,7 @@ import ( feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) @@ -31,8 +32,7 @@ type suggestedPriceConfig interface { BumpMin() *assets.Wei } -//go:generate mockery --quiet --name rpcClient --output ./mocks/ --case=underscore --structname RPCClient -type rpcClient interface { +type suggestedPriceEstimatorClient interface { CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error } @@ -41,7 +41,7 @@ type SuggestedPriceEstimator struct { services.StateMachine cfg suggestedPriceConfig - client rpcClient + client suggestedPriceEstimatorClient pollPeriod time.Duration logger logger.Logger @@ -52,10 +52,12 @@ type SuggestedPriceEstimator struct { chInitialised chan struct{} chStop services.StopChan chDone chan struct{} + + l1Oracle rollups.L1Oracle } // NewSuggestedPriceEstimator returns a new Estimator which uses the suggested gas price. -func NewSuggestedPriceEstimator(lggr logger.Logger, client rpcClient, cfg suggestedPriceConfig) EvmEstimator { +func NewSuggestedPriceEstimator(lggr logger.Logger, client feeEstimatorClient, cfg suggestedPriceConfig, l1Oracle rollups.L1Oracle) EvmEstimator { return &SuggestedPriceEstimator{ client: client, pollPeriod: 10 * time.Second, @@ -65,6 +67,7 @@ func NewSuggestedPriceEstimator(lggr logger.Logger, client rpcClient, cfg sugges chInitialised: make(chan struct{}), chStop: make(chan struct{}), chDone: make(chan struct{}), + l1Oracle: l1Oracle, } } @@ -72,6 +75,10 @@ func (o *SuggestedPriceEstimator) Name() string { return o.logger.Name() } +func (o *SuggestedPriceEstimator) L1Oracle() rollups.L1Oracle { + return o.l1Oracle +} + func (o *SuggestedPriceEstimator) Start(context.Context) error { return o.StartOnce("SuggestedPriceEstimator", func() error { go o.run() diff --git a/core/chains/evm/gas/suggested_price_estimator_test.go b/core/chains/evm/gas/suggested_price_estimator_test.go index 0d52d6ab1b9..4f3c4d307d6 100644 --- a/core/chains/evm/gas/suggested_price_estimator_test.go +++ b/core/chains/evm/gas/suggested_price_estimator_test.go @@ -15,6 +15,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" + rollupMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) @@ -29,20 +30,24 @@ func TestSuggestedPriceEstimator(t *testing.T) { cfg := &gas.MockGasEstimatorConfig{BumpPercentF: 10, BumpMinF: assets.NewWei(big.NewInt(1)), BumpThresholdF: 1} t.Run("calling GetLegacyGas on unstarted estimator returns error", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) _, _, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) assert.EqualError(t, err, "estimator is not started") }) t.Run("calling GetLegacyGas on started estimator returns prices", func(t *testing.T) { - client := mocks.NewRPCClient(t) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(42) }) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) servicetest.RunHealthy(t, o) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) require.NoError(t, err) @@ -51,10 +56,12 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("gas price is lower than user specified max gas price", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(42) }) @@ -68,10 +75,12 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("gas price is lower than global max gas price", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(120) }) @@ -84,10 +93,12 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("calling GetLegacyGas on started estimator if initial call failed returns error", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(pkgerrors.New("kaboom")) + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(pkgerrors.New("kaboom")) servicetest.RunHealthy(t, o) @@ -96,22 +107,28 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("calling GetDynamicFee always returns error", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) _, err := o.GetDynamicFee(testutils.Context(t), maxGasPrice) assert.EqualError(t, err, "dynamic fees are not implemented for this estimator") }) t.Run("calling BumpLegacyGas on unstarted estimator returns error", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) _, _, err := o.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), gasLimit, maxGasPrice, nil) assert.EqualError(t, err, "estimator is not started") }) t.Run("calling BumpDynamicFee always returns error", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) fee := gas.DynamicFee{ FeeCap: assets.NewWeiI(42), TipCap: assets.NewWeiI(5), @@ -121,13 +138,15 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("calling BumpLegacyGas on started estimator returns new price buffered with bumpPercent", func(t *testing.T) { - client := mocks.NewRPCClient(t) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(40) }) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) servicetest.RunHealthy(t, o) gasPrice, chainSpecificGasLimit, err := o.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(10), gasLimit, maxGasPrice, nil) require.NoError(t, err) @@ -136,14 +155,16 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("calling BumpLegacyGas on started estimator returns new price buffered with bumpMin", func(t *testing.T) { - client := mocks.NewRPCClient(t) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(40) }) - testCfg := &gas.MockGasEstimatorConfig{BumpPercentF: 1, BumpMinF: assets.NewWei(big.NewInt(1)), BumpThresholdF: 1} - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, testCfg) + testCfg := &gas.MockGasEstimatorConfig{BumpPercentF: 1, BumpMinF: assets.NewWei(big.NewInt(1)), BumpThresholdF: 1, LimitMultiplierF: 1} + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, testCfg, l1Oracle) servicetest.RunHealthy(t, o) gasPrice, chainSpecificGasLimit, err := o.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(10), gasLimit, maxGasPrice, nil) require.NoError(t, err) @@ -152,13 +173,15 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("calling BumpLegacyGas on started estimator returns original price when lower than previous", func(t *testing.T) { - client := mocks.NewRPCClient(t) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(5) }) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) servicetest.RunHealthy(t, o) gasPrice, chainSpecificGasLimit, err := o.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(10), gasLimit, maxGasPrice, nil) require.NoError(t, err) @@ -167,10 +190,12 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("calling BumpLegacyGas on started estimator returns error, suggested gas price is higher than max gas price", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(42) }) @@ -184,10 +209,12 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("calling BumpLegacyGas on started estimator returns max gas price when suggested price under max but the buffer exceeds it", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) + + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(39) }) @@ -200,10 +227,12 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("calling BumpLegacyGas on started estimator if initial call failed returns error", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(pkgerrors.New("kaboom")) + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(pkgerrors.New("kaboom")) servicetest.RunHealthy(t, o) @@ -212,14 +241,16 @@ func TestSuggestedPriceEstimator(t *testing.T) { }) t.Run("calling BumpLegacyGas on started estimator if refresh call failed returns price from previous update", func(t *testing.T) { - client := mocks.NewRPCClient(t) - o := gas.NewSuggestedPriceEstimator(logger.Test(t), client, cfg) + feeEstimatorClient := mocks.NewFeeEstimatorClient(t) + l1Oracle := rollupMocks.NewL1Oracle(t) + + o := gas.NewSuggestedPriceEstimator(logger.Test(t), feeEstimatorClient, cfg, l1Oracle) - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) (*big.Int)(res).SetInt64(40) }).Once() - client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(pkgerrors.New("kaboom")) + feeEstimatorClient.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(pkgerrors.New("kaboom")) servicetest.RunHealthy(t, o) diff --git a/core/chains/evm/txmgr/broadcaster_test.go b/core/chains/evm/txmgr/broadcaster_test.go index 3500002e8da..070c2c37473 100644 --- a/core/chains/evm/txmgr/broadcaster_test.go +++ b/core/chains/evm/txmgr/broadcaster_test.go @@ -64,9 +64,9 @@ func NewTestEthBroadcaster( lggr := logger.Test(t) ge := config.EVM().GasEstimator() - estimator := gas.NewWrappedEvmEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { - return gas.NewFixedPriceEstimator(config.EVM().GasEstimator(), ge.BlockHistory(), lggr) - }, ge.EIP1559DynamicFees(), nil, ge) + estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { + return gas.NewFixedPriceEstimator(config.EVM().GasEstimator(), nil, ge.BlockHistory(), lggr, nil) + }, ge.EIP1559DynamicFees(), ge) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, keyStore, estimator) ethBroadcaster := txmgrcommon.NewBroadcaster(txStore, txmgr.NewEvmTxmClient(ethClient), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(config.EVM().GasEstimator()), config.EVM().Transactions(), config.Database().Listener(), keyStore, txBuilder, nonceTracker, lggr, checkerFactory, nonceAutoSync) @@ -1152,9 +1152,9 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_Errors(t *testing.T) { // same as the parent test, but callback is set by ctor t.Run("callback set by ctor", func(t *testing.T) { - estimator := gas.NewWrappedEvmEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { - return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), evmcfg.EVM().GasEstimator().BlockHistory(), lggr) - }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), nil, evmcfg.EVM().GasEstimator()) + estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { + return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, evmcfg.EVM().GasEstimator().BlockHistory(), lggr, nil) + }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator()) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), evmcfg.EVM().GasEstimator(), ethKeyStore, estimator) localNextNonce = getLocalNextNonce(t, nonceTracker, fromAddress) eb2 := txmgr.NewEvmBroadcaster(txStore, txmClient, txmgr.NewEvmTxmConfig(evmcfg.EVM()), txmgr.NewEvmTxmFeeConfig(evmcfg.EVM().GasEstimator()), evmcfg.EVM().Transactions(), evmcfg.Database().Listener(), ethKeyStore, txBuilder, lggr, &testCheckerFactory{}, false) @@ -1738,9 +1738,9 @@ func TestEthBroadcaster_SyncNonce(t *testing.T) { kst := cltest.NewKeyStore(t, db, cfg.Database()).Eth() _, fromAddress := cltest.RandomKey{Disabled: false}.MustInsertWithState(t, kst) - estimator := gas.NewWrappedEvmEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { - return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), evmcfg.EVM().GasEstimator().BlockHistory(), lggr) - }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), nil, evmcfg.EVM().GasEstimator()) + estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { + return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, evmcfg.EVM().GasEstimator().BlockHistory(), lggr, nil) + }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator()) checkerFactory := &testCheckerFactory{} ge := evmcfg.EVM().GasEstimator() diff --git a/core/chains/evm/txmgr/confirmer_test.go b/core/chains/evm/txmgr/confirmer_test.go index 357dafcbdc4..89e88d5a6dc 100644 --- a/core/chains/evm/txmgr/confirmer_test.go +++ b/core/chains/evm/txmgr/confirmer_test.go @@ -127,7 +127,7 @@ func TestEthConfirmer_Lifecycle(t *testing.T) { newEst := func(logger.Logger) gas.EvmEstimator { return estimator } lggr := logger.Test(t) ge := config.EVM().GasEstimator() - feeEstimator := gas.NewWrappedEvmEstimator(lggr, newEst, ge.EIP1559DynamicFees(), nil, ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, feeEstimator) ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), config.Database(), ethKeyStore, txBuilder, lggr) ctx := testutils.Context(t) @@ -1647,7 +1647,7 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing newEst := func(logger.Logger) gas.EvmEstimator { return estimator } estimator.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, uint64(0), pkgerrors.Wrapf(commonfee.ErrConnectivity, "transaction...")) ge := ccfg.EVM().GasEstimator() - feeEstimator := gas.NewWrappedEvmEstimator(lggr, newEst, ge.EIP1559DynamicFees(), nil, ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, feeEstimator) addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", mock.Anything, &cltest.FixtureChainID).Return(addresses, nil).Maybe() @@ -1695,7 +1695,7 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing newEst := func(logger.Logger) gas.EvmEstimator { return estimator } // Create confirmer with necessary state ge := ccfg.EVM().GasEstimator() - feeEstimator := gas.NewWrappedEvmEstimator(lggr, newEst, ge.EIP1559DynamicFees(), nil, ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, feeEstimator) addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", mock.Anything, &cltest.FixtureChainID).Return(addresses, nil).Maybe() @@ -3133,9 +3133,9 @@ func ptr[T any](t T) *T { return &t } func newEthConfirmer(t testing.TB, txStore txmgr.EvmTxStore, ethClient client.Client, config evmconfig.ChainScopedConfig, ks keystore.Eth, fn txmgrcommon.ResumeCallback) *txmgr.Confirmer { lggr := logger.Test(t) ge := config.EVM().GasEstimator() - estimator := gas.NewWrappedEvmEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { - return gas.NewFixedPriceEstimator(ge, ge.BlockHistory(), lggr) - }, ge.EIP1559DynamicFees(), nil, ge) + estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { + return gas.NewFixedPriceEstimator(ge, nil, ge.BlockHistory(), lggr, nil) + }, ge.EIP1559DynamicFees(), ge) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ks, estimator) ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), config.Database(), ks, txBuilder, lggr) ec.SetResumeCallback(fn)