From af3e893828bd0f38c88c9ccdbb0b3c4eeffde057 Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Fri, 8 Sep 2023 02:01:46 -0400 Subject: [PATCH 01/14] Draft impl --- core/chains/evm/gas/arbitrum_estimator.go | 19 ++- .../chains/evm/gas/block_history_estimator.go | 31 +++-- core/chains/evm/gas/fixed_price_estimator.go | 17 ++- .../evm/gas/fixed_price_estimator_test.go | 13 ++- core/chains/evm/gas/l2_suggested_estimator.go | 35 ++++-- .../chains/evm/gas/mocks/evm_fee_estimator.go | 33 ++++++ core/chains/evm/gas/models.go | 18 ++- core/chains/evm/gas/price_models.go | 29 +++++ .../evm/gas/prices/gas_oracle_getter.go | 108 ++++++++++++++++++ core/chains/evm/gas/prices/no_op_getter.go | 27 +++++ 10 files changed, 296 insertions(+), 34 deletions(-) create mode 100644 core/chains/evm/gas/price_models.go create mode 100644 core/chains/evm/gas/prices/gas_oracle_getter.go create mode 100644 core/chains/evm/gas/prices/no_op_getter.go diff --git a/core/chains/evm/gas/arbitrum_estimator.go b/core/chains/evm/gas/arbitrum_estimator.go index df0c4b8f8cb..6b656deb61d 100644 --- a/core/chains/evm/gas/arbitrum_estimator.go +++ b/core/chains/evm/gas/arbitrum_estimator.go @@ -51,11 +51,11 @@ type arbitrumEstimator struct { utils.StartStopOnce } -func NewArbitrumEstimator(lggr logger.Logger, cfg ArbConfig, rpcClient rpcClient, ethClient ethClient) EvmEstimator { +func NewArbitrumEstimator(lggr logger.Logger, cfg ArbConfig, rpcClient rpcClient, ethClient ethClient, p PriceComponentGetter) EvmEstimator { lggr = lggr.Named("ArbitrumEstimator") return &arbitrumEstimator{ cfg: cfg, - EvmEstimator: NewL2SuggestedPriceEstimator(lggr, rpcClient), + EvmEstimator: NewL2SuggestedPriceEstimator(lggr, rpcClient, p), client: ethClient, pollPeriod: 10 * time.Second, logger: lggr, @@ -95,6 +95,21 @@ func (a *arbitrumEstimator) HealthReport() map[string]error { return map[string]error{a.Name(): a.StartStopOnce.Healthy()} } +func (a *arbitrumEstimator) GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) { + prices, err = a.EvmEstimator.GetPriceComponents(ctx, maxGasPriceWei, opts...) + if err != nil { + return + } + + gasPrice, _, err := a.GetLegacyGas(ctx, nil, 0, maxGasPriceWei, opts...) + if err != nil { + return []PriceComponent{}, err + } + + prices[0].Price = gasPrice + return +} + // GetLegacyGas estimates both the gas price and the gas limit. // - Price is delegated to the embedded l2SuggestedPriceEstimator. // - Limit is computed from the dynamic values perL2Tx and perL1CalldataUnit, provided by the getPricesInArbGas() method diff --git a/core/chains/evm/gas/block_history_estimator.go b/core/chains/evm/gas/block_history_estimator.go index 556c0b1715c..370738103f9 100644 --- a/core/chains/evm/gas/block_history_estimator.go +++ b/core/chains/evm/gas/block_history_estimator.go @@ -119,14 +119,15 @@ type ( latestMu sync.RWMutex initialFetch atomic.Bool - logger logger.SugaredLogger + logger logger.SugaredLogger + priceComponentGetter PriceComponentGetter } ) // 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 evmclient.Client, cfg chainConfig, eCfg estimatorGasEstimatorConfig, bhCfg BlockHistoryConfig, chainID big.Int, p PriceComponentGetter) EvmEstimator { ctx, cancel := context.WithCancel(context.Background()) b := &BlockHistoryEstimator{ ethClient: ethClient, @@ -136,12 +137,13 @@ func NewBlockHistoryEstimator(lggr logger.Logger, ethClient evmclient.Client, cf bhConfig: bhCfg, blocks: make([]evmtypes.Block, 0), // Must have enough blocks for both estimator and connectivity checker - size: int64(mathutil.Max(bhCfg.BlockHistorySize(), bhCfg.CheckInclusionBlocks())), - mb: utils.NewSingleMailbox[*evmtypes.Head](), - wg: new(sync.WaitGroup), - ctx: ctx, - ctxCancel: cancel, - logger: logger.Sugared(lggr.Named("BlockHistoryEstimator")), + size: int64(mathutil.Max(bhCfg.BlockHistorySize(), bhCfg.CheckInclusionBlocks())), + mb: utils.NewSingleMailbox[*evmtypes.Head](), + wg: new(sync.WaitGroup), + ctx: ctx, + ctxCancel: cancel, + logger: logger.Sugared(lggr.Named("BlockHistoryEstimator")), + priceComponentGetter: p, } return b @@ -246,6 +248,14 @@ func (b *BlockHistoryEstimator) HealthReport() map[string]error { return map[string]error{b.Name(): b.StartStopOnce.Healthy()} } +func (b *BlockHistoryEstimator) GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) { + gasPrice, _, err := b.GetLegacyGas(ctx, nil, 0, maxGasPriceWei, opts...) + if err != nil { + return []PriceComponent{}, err + } + return b.priceComponentGetter.GetPriceComponents(ctx, gasPrice) +} + func (b *BlockHistoryEstimator) GetLegacyGas(_ context.Context, _ []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, _ ...feetypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { ok := b.IfStarted(func() { gasPrice = b.getGasPrice() @@ -500,6 +510,11 @@ func (b *BlockHistoryEstimator) FetchBlocksAndRecalculate(ctx context.Context, h } b.initialFetch.Store(true) b.Recalculate(head) + + if err := b.priceComponentGetter.RefreshComponents(b.ctx); err != nil { + b.logger.Warnw("Error refreshing price components", "err", err) + return + } } // Recalculate adds the given heads to the history and recalculates gas price. diff --git a/core/chains/evm/gas/fixed_price_estimator.go b/core/chains/evm/gas/fixed_price_estimator.go index 733dd179fec..029ea377bf9 100644 --- a/core/chains/evm/gas/fixed_price_estimator.go +++ b/core/chains/evm/gas/fixed_price_estimator.go @@ -4,6 +4,7 @@ import ( "context" "github.com/pkg/errors" + commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" "github.com/smartcontractkit/chainlink/v2/core/assets" @@ -14,9 +15,10 @@ import ( var _ EvmEstimator = (*fixedPriceEstimator)(nil) type fixedPriceEstimator struct { - config fixedPriceEstimatorConfig - bhConfig fixedPriceEstimatorBlockHistoryConfig - lggr logger.SugaredLogger + config fixedPriceEstimatorConfig + bhConfig fixedPriceEstimatorBlockHistoryConfig + lggr logger.SugaredLogger + priceComponentGetter PriceComponentGetter } 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(lggr.Named("FixedPriceEstimator"))} +func NewFixedPriceEstimator(cfg fixedPriceEstimatorConfig, bhCfg fixedPriceEstimatorBlockHistoryConfig, lggr logger.Logger, p PriceComponentGetter) EvmEstimator { + return &fixedPriceEstimator{cfg, bhCfg, logger.Sugared(lggr.Named("FixedPriceEstimator")), p} } func (f *fixedPriceEstimator) Start(context.Context) error { @@ -57,6 +59,11 @@ func (f *fixedPriceEstimator) Start(context.Context) error { return nil } +func (f *fixedPriceEstimator) GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, _ ...feetypes.Opt) (prices []PriceComponent, err error) { + gasPrice := commonfee.CalculateFee(f.config.PriceDefault().ToInt(), maxGasPriceWei.ToInt(), f.config.PriceMax().ToInt()) + return f.priceComponentGetter.GetPriceComponents(ctx, assets.NewWei(gasPrice)) +} + func (f *fixedPriceEstimator) GetLegacyGas(_ context.Context, _ []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, _ ...feetypes.Opt) (*assets.Wei, uint32, error) { gasPrice := commonfee.CalculateFee(f.config.PriceDefault().ToInt(), maxGasPriceWei.ToInt(), f.config.PriceMax().ToInt()) chainSpecificGasLimit, err := commonfee.ApplyMultiplier(gasLimit, f.config.LimitMultiplier()) diff --git a/core/chains/evm/gas/fixed_price_estimator_test.go b/core/chains/evm/gas/fixed_price_estimator_test.go index a6cb313e5e2..da50a3eb816 100644 --- a/core/chains/evm/gas/fixed_price_estimator_test.go +++ b/core/chains/evm/gas/fixed_price_estimator_test.go @@ -8,6 +8,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/prices" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -26,7 +27,7 @@ func Test_FixedPriceEstimator(t *testing.T) { t.Run("GetLegacyGas returns EvmGasPriceDefault from config, with multiplier applied", func(t *testing.T) { config := &gas.MockGasEstimatorConfig{} - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t)) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t), prices.NewNoOpGetter()) config.PriceDefaultF = assets.NewWeiI(42) config.LimitMultiplierF = float32(1.1) @@ -43,7 +44,7 @@ func Test_FixedPriceEstimator(t *testing.T) { config.PriceDefaultF = assets.NewWeiI(42) config.LimitMultiplierF = float32(1.1) config.PriceMaxF = assets.NewWeiI(35) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t)) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t), prices.NewNoOpGetter()) gasPrice, gasLimit, err := f.GetLegacyGas(testutils.Context(t), nil, 100000, assets.NewWeiI(30)) require.NoError(t, err) @@ -57,7 +58,7 @@ func Test_FixedPriceEstimator(t *testing.T) { config.LimitMultiplierF = float32(1.1) config.PriceMaxF = assets.NewWeiI(20) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t)) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t), prices.NewNoOpGetter()) gasPrice, gasLimit, err := f.GetLegacyGas(testutils.Context(t), nil, 100000, assets.NewWeiI(30)) require.NoError(t, err) assert.Equal(t, 110000, int(gasLimit)) @@ -73,7 +74,7 @@ func Test_FixedPriceEstimator(t *testing.T) { config.BumpMinF = assets.NewWeiI(150) lggr := logger.TestLogger(t) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr, prices.NewNoOpGetter()) gasPrice, gasLimit, err := f.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), 100000, maxGasPrice, nil) require.NoError(t, err) @@ -94,7 +95,7 @@ func Test_FixedPriceEstimator(t *testing.T) { config.BumpThresholdF = uint64(3) lggr := logger.TestLogger(t) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr, prices.NewNoOpGetter()) fee, gasLimit, err := f.GetDynamicFee(testutils.Context(t), 100000, maxGasPrice) require.NoError(t, err) @@ -131,7 +132,7 @@ func Test_FixedPriceEstimator(t *testing.T) { config.BumpPercentF = uint16(10) lggr := logger.TestLogger(t) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr, prices.NewNoOpGetter()) originalFee := gas.DynamicFee{FeeCap: assets.NewWeiI(100), TipCap: assets.NewWeiI(25)} fee, gasLimit, err := f.BumpDynamicFee(testutils.Context(t), originalFee, 100000, maxGasPrice, nil) diff --git a/core/chains/evm/gas/l2_suggested_estimator.go b/core/chains/evm/gas/l2_suggested_estimator.go index e59eca2b064..cd719df287b 100644 --- a/core/chains/evm/gas/l2_suggested_estimator.go +++ b/core/chains/evm/gas/l2_suggested_estimator.go @@ -41,18 +41,21 @@ type l2SuggestedPriceEstimator struct { chInitialised chan struct{} chStop utils.StopChan chDone chan struct{} + + priceComponentGetter PriceComponentGetter } // NewL2SuggestedPriceEstimator returns a new Estimator which uses the L2 suggested gas price. -func NewL2SuggestedPriceEstimator(lggr logger.Logger, client rpcClient) EvmEstimator { +func NewL2SuggestedPriceEstimator(lggr logger.Logger, client rpcClient, p PriceComponentGetter) EvmEstimator { return &l2SuggestedPriceEstimator{ - client: client, - pollPeriod: 10 * time.Second, - logger: lggr.Named("L2SuggestedEstimator"), - chForceRefetch: make(chan (chan struct{})), - chInitialised: make(chan struct{}), - chStop: make(chan struct{}), - chDone: make(chan struct{}), + client: client, + pollPeriod: 10 * time.Second, + logger: lggr.Named("L2SuggestedEstimator"), + chForceRefetch: make(chan (chan struct{})), + chInitialised: make(chan struct{}), + chStop: make(chan struct{}), + chDone: make(chan struct{}), + priceComponentGetter: p, } } @@ -117,6 +120,14 @@ func (o *l2SuggestedPriceEstimator) refreshPrice() (t *time.Timer) { o.gasPriceMu.Lock() defer o.gasPriceMu.Unlock() o.l2GasPrice = bi + + priceGetterCtx, priceGetterCancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer priceGetterCancel() + if err := o.priceComponentGetter.RefreshComponents(priceGetterCtx); err != nil { + o.logger.Warnf("Failed to refresh price component getter, got error: %s", err) + return + } + return } @@ -132,6 +143,14 @@ func (*l2SuggestedPriceEstimator) BumpDynamicFee(_ context.Context, _ DynamicFee return } +func (f *l2SuggestedPriceEstimator) GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) { + gasPrice, _, err := f.GetLegacyGas(ctx, nil, 0, maxGasPriceWei, opts...) + if err != nil { + return []PriceComponent{}, err + } + return f.priceComponentGetter.GetPriceComponents(ctx, gasPrice) +} + func (o *l2SuggestedPriceEstimator) GetLegacyGas(ctx context.Context, _ []byte, l2GasLimit uint32, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { chainSpecificGasLimit = l2GasLimit diff --git a/core/chains/evm/gas/mocks/evm_fee_estimator.go b/core/chains/evm/gas/mocks/evm_fee_estimator.go index 20e6c940f7e..89fd8ecc71e 100644 --- a/core/chains/evm/gas/mocks/evm_fee_estimator.go +++ b/core/chains/evm/gas/mocks/evm_fee_estimator.go @@ -139,6 +139,39 @@ func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, ca return r0, r1 } +// GetPrices provides a mock function with given fields: ctx, maxGasPriceWei, opts +func (_m *EvmFeeEstimator) GetPrices(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...types.Opt) ([]gas.PriceComponent, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, maxGasPriceWei) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []gas.PriceComponent + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *assets.Wei, ...types.Opt) ([]gas.PriceComponent, error)); ok { + return rf(ctx, maxGasPriceWei, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *assets.Wei, ...types.Opt) []gas.PriceComponent); ok { + r0 = rf(ctx, maxGasPriceWei, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]gas.PriceComponent) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *assets.Wei, ...types.Opt) error); ok { + r1 = rf(ctx, maxGasPriceWei, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // HealthReport provides a mock function with given fields: func (_m *EvmFeeEstimator) HealthReport() map[string]error { ret := _m.Called() diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index 8b6580685b5..26862e28ba9 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -15,6 +15,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/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/prices" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/label" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/config" @@ -30,6 +31,7 @@ type EvmFeeEstimator interface { services.ServiceCtx commontypes.HeadTrackable[*evmtypes.Head, common.Hash] + GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) GetFee(ctx context.Context, calldata []byte, feeLimit uint32, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint32, err error) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint32, maxFeePrice *assets.Wei, attempts []EvmPriorAttempt) (bumpedFee EvmFee, chainSpecificFeeLimit uint32, err error) @@ -63,16 +65,16 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, ge df := geCfg.EIP1559DynamicFees() switch s { case "Arbitrum": - return NewWrappedEvmEstimator(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient), df) + return NewWrappedEvmEstimator(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient, prices.NewNoOpGetter()), df) case "BlockHistory": - return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()), df) + return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID(), prices.NewNoOpGetter()), df) case "FixedPrice": - return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df) + return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr, prices.NewNoOpGetter()), df) case "Optimism2", "L2Suggested": - return NewWrappedEvmEstimator(NewL2SuggestedPriceEstimator(lggr, ethClient), df) + return NewWrappedEvmEstimator(NewL2SuggestedPriceEstimator(lggr, ethClient, prices.NewNoOpGetter()), df) default: lggr.Warnf("GasEstimator: unrecognised mode '%s', falling back to FixedPriceEstimator", s) - return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df) + return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr, prices.NewNoOpGetter()), df) } } @@ -98,6 +100,8 @@ type EvmEstimator interface { commontypes.HeadTrackable[*evmtypes.Head, common.Hash] services.ServiceCtx + // GetPriceComponents returns chain-specific price components + GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) // GetLegacyGas Calculates initial gas fee for non-EIP1559 transaction // maxGasPriceWei parameter is the highest possible gas fee cap that the function will return GetLegacyGas(ctx context.Context, calldata []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) @@ -152,6 +156,10 @@ func NewWrappedEvmEstimator(e EvmEstimator, eip1559Enabled bool) EvmFeeEstimator } } +func (e WrappedEvmEstimator) GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) { + return e.EvmEstimator.GetPriceComponents(ctx, maxGasPriceWei, opts...) +} + func (e WrappedEvmEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint32, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint32, err error) { // get dynamic fee if e.EIP1559Enabled { diff --git a/core/chains/evm/gas/price_models.go b/core/chains/evm/gas/price_models.go new file mode 100644 index 00000000000..da7f921ea3b --- /dev/null +++ b/core/chains/evm/gas/price_models.go @@ -0,0 +1,29 @@ +package gas + +import ( + "context" + + "github.com/smartcontractkit/chainlink/v2/core/assets" +) + +type PriceType string + +const ( + GAS_PRICE PriceType = "GAS_PRICE" + L1_BASE_FEE PriceType = "L1_BASE_FEE" +) + +type PriceComponent struct { + Price *assets.Wei + PriceType PriceType +} + +// PriceComponentGetter provides interface for implementing chain-specific price components +// On L1 chains like Ethereum or Avax, the only component is the gas price. +// On Optimistic Rollups, there are two components - the L2 gas price, and L1 base fee for data availability. +// On future chains, there could be more or differing price components. +type PriceComponentGetter interface { + RefreshComponents(ctx context.Context) error + // GetPriceComponents The first component in prices should always be the passed-in gasPrice + GetPriceComponents(ctx context.Context, gasPrice *assets.Wei) (prices []PriceComponent, err error) +} diff --git a/core/chains/evm/gas/prices/gas_oracle_getter.go b/core/chains/evm/gas/prices/gas_oracle_getter.go new file mode 100644 index 00000000000..fd88108c596 --- /dev/null +++ b/core/chains/evm/gas/prices/gas_oracle_getter.go @@ -0,0 +1,108 @@ +package prices + +import ( + "context" + "fmt" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/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/logger" +) + +type OracleType string + +const ( + ARBITRUM OracleType = "ARBITRUM" + OP_STACK OracleType = "OP_STACK" +) + +type gasOracleGetter struct { + client evmclient.Client + logger logger.Logger + address string + selector string + componentPriceType gas.PriceType + + componentPriceMu sync.RWMutex + componentPrice *assets.Wei +} + +const ( + // ArbGasInfoAddress is the address of the "Precompiled contract that exists in every Arbitrum chain." + // https://github.com/OffchainLabs/nitro/blob/f7645453cfc77bf3e3644ea1ac031eff629df325/contracts/src/precompiles/ArbGasInfo.sol + ArbGasInfoAddress = "0x000000000000000000000000000000000000006C" + // ArbGasInfo_getL1BaseFeeEstimate is the a hex encoded call to: + // `function getL1BaseFeeEstimate() external view returns (uint256);` + ArbGasInfo_getL1BaseFeeEstimate = "f5d6ded7" + + // GasOracleAddress is the address of the "Precompiled contract that exists in every OP stack chain." + OPGasOracleAddress = "0x420000000000000000000000000000000000000F" + // GasOracle_l1BaseFee is the a hex encoded call to: + // `function l1BaseFee() external view returns (uint256);` + OPGasOracle_getL1BaseFeeEstimate = "519b4bd3" +) + +func NewGasOracleGetter(lggr logger.Logger, ethClient evmclient.Client, oracleType OracleType, componentPriceType gas.PriceType) gas.PriceComponentGetter { + var address, selector string + switch oracleType { + case ARBITRUM: + address = ArbGasInfoAddress + selector = ArbGasInfo_getL1BaseFeeEstimate + case OP_STACK: + address = OPGasOracleAddress + selector = OPGasOracle_getL1BaseFeeEstimate + default: + panic(fmt.Errorf("unsupportd oracle type: %s", oracleType)) + } + + return &gasOracleGetter{ + client: ethClient, + logger: lggr.Named("NewGasOracleGetter"), + address: address, + selector: selector, + componentPriceType: componentPriceType, + } +} + +func (o *gasOracleGetter) RefreshComponents(ctx context.Context) (err error) { + precompile := common.HexToAddress(o.address) + b, err := o.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: common.Hex2Bytes(o.selector), + }, big.NewInt(-1)) + if err != nil { + return err + } + + if len(b) != 32 { // returns uint256; + return fmt.Errorf("return data length (%d) different than expected (%d)", len(b), 32) + } + price := new(big.Int).SetBytes(b) + + o.componentPriceMu.Lock() + defer o.componentPriceMu.Unlock() + o.componentPrice = assets.NewWei(price) + + return +} + +func (o *gasOracleGetter) GetPriceComponents(_ context.Context, gasPrice *assets.Wei) (prices []gas.PriceComponent, err error) { + o.componentPriceMu.RLock() + defer o.componentPriceMu.RUnlock() + return []gas.PriceComponent{ + { + Price: gasPrice, + PriceType: gas.GAS_PRICE, + }, + { + Price: o.componentPrice, + PriceType: o.componentPriceType, + }, + }, nil +} diff --git a/core/chains/evm/gas/prices/no_op_getter.go b/core/chains/evm/gas/prices/no_op_getter.go new file mode 100644 index 00000000000..f9e39b5d470 --- /dev/null +++ b/core/chains/evm/gas/prices/no_op_getter.go @@ -0,0 +1,27 @@ +package prices + +import ( + "context" + + "github.com/smartcontractkit/chainlink/v2/core/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" +) + +type noOpGetter struct{} + +func NewNoOpGetter() gas.PriceComponentGetter { + return &noOpGetter{} +} + +func (e *noOpGetter) RefreshComponents(_ context.Context) error { + return nil +} + +func (e *noOpGetter) GetPriceComponents(_ context.Context, gasPrice *assets.Wei) (prices []gas.PriceComponent, err error) { + return []gas.PriceComponent{ + { + Price: gasPrice, + PriceType: gas.GAS_PRICE, + }, + }, nil +} From 33af306c546d1d778fa33e2f2626c19ff9ad909c Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Fri, 8 Sep 2023 02:16:49 -0400 Subject: [PATCH 02/14] update configs --- .../evm/config/toml/defaults/Base_Goerli.toml | 1 + .../evm/config/toml/defaults/Base_Mainnet.toml | 1 + .../config/toml/defaults/Optimism_Goerli.toml | 1 + .../config/toml/defaults/Optimism_Mainnet.toml | 1 + core/chains/evm/gas/arbitrum_estimator_test.go | 17 +++++++++-------- .../evm/gas/block_history_estimator_test.go | 5 +++-- .../evm/gas/l2_suggested_estimator_test.go | 13 +++++++------ core/chains/evm/gas/models.go | 6 +++++- 8 files changed, 28 insertions(+), 17 deletions(-) diff --git a/core/chains/evm/config/toml/defaults/Base_Goerli.toml b/core/chains/evm/config/toml/defaults/Base_Goerli.toml index 5ecfd036f46..092974b0aee 100644 --- a/core/chains/evm/config/toml/defaults/Base_Goerli.toml +++ b/core/chains/evm/config/toml/defaults/Base_Goerli.toml @@ -6,6 +6,7 @@ NoNewHeadsThreshold = '40s' MinIncomingConfirmations = 1 [GasEstimator] +Mode = 'OPStack' EIP1559DynamicFees = true PriceMin = '1 wei' BumpMin = '100 wei' diff --git a/core/chains/evm/config/toml/defaults/Base_Mainnet.toml b/core/chains/evm/config/toml/defaults/Base_Mainnet.toml index 314c12f8c54..b724b01db30 100644 --- a/core/chains/evm/config/toml/defaults/Base_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Base_Mainnet.toml @@ -6,6 +6,7 @@ NoNewHeadsThreshold = '40s' MinIncomingConfirmations = 1 [GasEstimator] +Mode = 'OPStack' EIP1559DynamicFees = true PriceMin = '1 wei' BumpMin = '100 wei' diff --git a/core/chains/evm/config/toml/defaults/Optimism_Goerli.toml b/core/chains/evm/config/toml/defaults/Optimism_Goerli.toml index 458b3b08123..21e488576fe 100644 --- a/core/chains/evm/config/toml/defaults/Optimism_Goerli.toml +++ b/core/chains/evm/config/toml/defaults/Optimism_Goerli.toml @@ -7,6 +7,7 @@ NoNewHeadsThreshold = '40s' MinIncomingConfirmations = 1 [GasEstimator] +Mode = 'OPStack' EIP1559DynamicFees = true PriceMin = '1 wei' BumpMin = '100 wei' diff --git a/core/chains/evm/config/toml/defaults/Optimism_Mainnet.toml b/core/chains/evm/config/toml/defaults/Optimism_Mainnet.toml index fd4dd9f32f0..bff70e35da1 100644 --- a/core/chains/evm/config/toml/defaults/Optimism_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Optimism_Mainnet.toml @@ -7,6 +7,7 @@ NoNewHeadsThreshold = '40s' MinIncomingConfirmations = 1 [GasEstimator] +Mode = 'OPStack' EIP1559DynamicFees = true PriceMin = '1 wei' BumpMin = '100 wei' diff --git a/core/chains/evm/gas/arbitrum_estimator_test.go b/core/chains/evm/gas/arbitrum_estimator_test.go index b6e299190c5..7a10c6fd44c 100644 --- a/core/chains/evm/gas/arbitrum_estimator_test.go +++ b/core/chains/evm/gas/arbitrum_estimator_test.go @@ -17,6 +17,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/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/prices" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -41,7 +42,7 @@ func TestArbitrumEstimator(t *testing.T) { 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.TestLogger(t), &arbConfig{}, rpcClient, ethClient) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, rpcClient, ethClient, prices.NewNoOpGetter()) _, _, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) assert.EqualError(t, err, "estimator is not started") }) @@ -65,7 +66,7 @@ func TestArbitrumEstimator(t *testing.T) { assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(zeros.Bytes(), nil) - o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient, prices.NewNoOpGetter()) require.NoError(t, o.Start(testutils.Context(t))) t.Cleanup(func() { assert.NoError(t, o.Close()) }) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) @@ -78,7 +79,7 @@ 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.TestLogger(t), &arbConfig{}, client, ethClient) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, client, ethClient, prices.NewNoOpGetter()) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) @@ -104,7 +105,7 @@ 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.TestLogger(t), &arbConfig{}, client, ethClient) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, client, ethClient, prices.NewNoOpGetter()) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) @@ -129,7 +130,7 @@ func TestArbitrumEstimator(t *testing.T) { t.Run("calling BumpLegacyGas always returns error", func(t *testing.T) { rpcClient := mocks.NewRPCClient(t) ethClient := mocks.NewETHClient(t) - o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, rpcClient, ethClient) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, rpcClient, ethClient, prices.NewNoOpGetter()) _, _, err := o.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), gasLimit, assets.NewWeiI(10), nil) assert.EqualError(t, err, "bump gas is not supported for this l2") }) @@ -137,7 +138,7 @@ func TestArbitrumEstimator(t *testing.T) { 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.TestLogger(t), &arbConfig{}, client, ethClient) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, client, ethClient, prices.NewNoOpGetter()) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(errors.New("kaboom")) ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { @@ -180,7 +181,7 @@ func TestArbitrumEstimator(t *testing.T) { assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(b.Bytes(), nil) - o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient, prices.NewNoOpGetter()) require.NoError(t, o.Start(testutils.Context(t))) t.Cleanup(func() { assert.NoError(t, o.Close()) }) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) @@ -215,7 +216,7 @@ func TestArbitrumEstimator(t *testing.T) { assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(b.Bytes(), nil) - o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient, prices.NewNoOpGetter()) require.NoError(t, o.Start(testutils.Context(t))) t.Cleanup(func() { assert.NoError(t, o.Close()) }) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) diff --git a/core/chains/evm/gas/block_history_estimator_test.go b/core/chains/evm/gas/block_history_estimator_test.go index c44e68002d2..e867c439fd9 100644 --- a/core/chains/evm/gas/block_history_estimator_test.go +++ b/core/chains/evm/gas/block_history_estimator_test.go @@ -22,6 +22,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/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/prices" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" @@ -41,7 +42,7 @@ func newBlockHistoryConfig() *gas.MockBlockHistoryConfig { } 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.TestLogger(t), c, cfg, gCfg, bhCfg, cid) + return gas.NewBlockHistoryEstimator(logger.TestLogger(t), c, cfg, gCfg, bhCfg, cid, prices.NewNoOpGetter()) } func newBlockHistoryEstimator(t *testing.T, c evmclient.Client, cfg gas.Config, gCfg gas.GasEstimatorConfig, bhCfg gas.BlockHistoryConfig) *gas.BlockHistoryEstimator { @@ -2019,7 +2020,7 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { geCfg.EIP1559DynamicFeesF = false bhe := gas.BlockHistoryEstimatorFromInterface( - gas.NewBlockHistoryEstimator(lggr, nil, cfg, geCfg, bhCfg, *testutils.NewRandomEVMChainID()), + gas.NewBlockHistoryEstimator(lggr, nil, cfg, geCfg, bhCfg, *testutils.NewRandomEVMChainID(), prices.NewNoOpGetter()), ) attempts := []gas.EvmPriorAttempt{ diff --git a/core/chains/evm/gas/l2_suggested_estimator_test.go b/core/chains/evm/gas/l2_suggested_estimator_test.go index 69b36033024..2a9e8188e9b 100644 --- a/core/chains/evm/gas/l2_suggested_estimator_test.go +++ b/core/chains/evm/gas/l2_suggested_estimator_test.go @@ -13,6 +13,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/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/prices" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -27,7 +28,7 @@ func TestL2SuggestedEstimator(t *testing.T) { t.Run("calling GetLegacyGas on unstarted estimator returns error", func(t *testing.T) { client := mocks.NewRPCClient(t) - o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) _, _, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) assert.EqualError(t, err, "estimator is not started") }) @@ -39,7 +40,7 @@ func TestL2SuggestedEstimator(t *testing.T) { (*big.Int)(res).SetInt64(42) }) - o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) require.NoError(t, o.Start(testutils.Context(t))) t.Cleanup(func() { assert.NoError(t, o.Close()) }) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) @@ -50,7 +51,7 @@ func TestL2SuggestedEstimator(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.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) @@ -68,7 +69,7 @@ func TestL2SuggestedEstimator(t *testing.T) { t.Run("gas price is lower than global max gas price", func(t *testing.T) { client := mocks.NewRPCClient(t) - o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) @@ -85,14 +86,14 @@ func TestL2SuggestedEstimator(t *testing.T) { t.Run("calling BumpLegacyGas always returns error", func(t *testing.T) { client := mocks.NewRPCClient(t) - o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) _, _, err := o.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), gasLimit, assets.NewWeiI(10), nil) assert.EqualError(t, err, "bump gas is not supported for this l2") }) t.Run("calling GetLegacyGas on started estimator if initial call failed returns error", func(t *testing.T) { client := mocks.NewRPCClient(t) - o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(errors.New("kaboom")) diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index 26862e28ba9..c3b44679e44 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -65,9 +65,13 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, ge df := geCfg.EIP1559DynamicFees() switch s { case "Arbitrum": - return NewWrappedEvmEstimator(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient, prices.NewNoOpGetter()), df) + p := prices.NewGasOracleGetter(lggr, ethClient, prices.ARBITRUM, L1_BASE_FEE) + return NewWrappedEvmEstimator(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient, p), df) case "BlockHistory": return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID(), prices.NewNoOpGetter()), df) + case "OPStack": + p := prices.NewGasOracleGetter(lggr, ethClient, prices.OP_STACK, L1_BASE_FEE) + return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID(), p), df) case "FixedPrice": return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr, prices.NewNoOpGetter()), df) case "Optimism2", "L2Suggested": From e5f5afff034661691223cf87c363fd3f76039603 Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Sat, 9 Sep 2023 23:54:22 -0400 Subject: [PATCH 03/14] move l1 knowledge from EVMEstimator to WrappedEVMFeeEstimator --- core/chains/evm/gas/arbitrum_estimator.go | 19 +-- .../chains/evm/gas/arbitrum_estimator_test.go | 17 +- .../chains/evm/gas/block_history_estimator.go | 31 +--- .../evm/gas/block_history_estimator_test.go | 5 +- .../gas/chainoracles/l1_base_fee_oracle.go | 155 ++++++++++++++++++ core/chains/evm/gas/chainoracles/models.go | 16 ++ core/chains/evm/gas/fixed_price_estimator.go | 16 +- .../evm/gas/fixed_price_estimator_test.go | 13 +- core/chains/evm/gas/l2_suggested_estimator.go | 35 +--- .../evm/gas/l2_suggested_estimator_test.go | 13 +- .../chains/evm/gas/mocks/evm_fee_estimator.go | 43 ++--- core/chains/evm/gas/models.go | 110 +++++++++++-- core/chains/evm/gas/price_models.go | 29 ---- .../evm/gas/prices/gas_oracle_getter.go | 108 ------------ core/chains/evm/gas/prices/no_op_getter.go | 27 --- 15 files changed, 322 insertions(+), 315 deletions(-) create mode 100644 core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go create mode 100644 core/chains/evm/gas/chainoracles/models.go delete mode 100644 core/chains/evm/gas/price_models.go delete mode 100644 core/chains/evm/gas/prices/gas_oracle_getter.go delete mode 100644 core/chains/evm/gas/prices/no_op_getter.go diff --git a/core/chains/evm/gas/arbitrum_estimator.go b/core/chains/evm/gas/arbitrum_estimator.go index 6b656deb61d..df0c4b8f8cb 100644 --- a/core/chains/evm/gas/arbitrum_estimator.go +++ b/core/chains/evm/gas/arbitrum_estimator.go @@ -51,11 +51,11 @@ type arbitrumEstimator struct { utils.StartStopOnce } -func NewArbitrumEstimator(lggr logger.Logger, cfg ArbConfig, rpcClient rpcClient, ethClient ethClient, p PriceComponentGetter) EvmEstimator { +func NewArbitrumEstimator(lggr logger.Logger, cfg ArbConfig, rpcClient rpcClient, ethClient ethClient) EvmEstimator { lggr = lggr.Named("ArbitrumEstimator") return &arbitrumEstimator{ cfg: cfg, - EvmEstimator: NewL2SuggestedPriceEstimator(lggr, rpcClient, p), + EvmEstimator: NewL2SuggestedPriceEstimator(lggr, rpcClient), client: ethClient, pollPeriod: 10 * time.Second, logger: lggr, @@ -95,21 +95,6 @@ func (a *arbitrumEstimator) HealthReport() map[string]error { return map[string]error{a.Name(): a.StartStopOnce.Healthy()} } -func (a *arbitrumEstimator) GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) { - prices, err = a.EvmEstimator.GetPriceComponents(ctx, maxGasPriceWei, opts...) - if err != nil { - return - } - - gasPrice, _, err := a.GetLegacyGas(ctx, nil, 0, maxGasPriceWei, opts...) - if err != nil { - return []PriceComponent{}, err - } - - prices[0].Price = gasPrice - return -} - // GetLegacyGas estimates both the gas price and the gas limit. // - Price is delegated to the embedded l2SuggestedPriceEstimator. // - Limit is computed from the dynamic values perL2Tx and perL1CalldataUnit, provided by the getPricesInArbGas() method diff --git a/core/chains/evm/gas/arbitrum_estimator_test.go b/core/chains/evm/gas/arbitrum_estimator_test.go index 7a10c6fd44c..b6e299190c5 100644 --- a/core/chains/evm/gas/arbitrum_estimator_test.go +++ b/core/chains/evm/gas/arbitrum_estimator_test.go @@ -17,7 +17,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/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/prices" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -42,7 +41,7 @@ func TestArbitrumEstimator(t *testing.T) { 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.TestLogger(t), &arbConfig{}, rpcClient, ethClient, prices.NewNoOpGetter()) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, rpcClient, ethClient) _, _, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) assert.EqualError(t, err, "estimator is not started") }) @@ -66,7 +65,7 @@ func TestArbitrumEstimator(t *testing.T) { assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(zeros.Bytes(), nil) - o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient, prices.NewNoOpGetter()) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient) require.NoError(t, o.Start(testutils.Context(t))) t.Cleanup(func() { assert.NoError(t, o.Close()) }) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) @@ -79,7 +78,7 @@ 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.TestLogger(t), &arbConfig{}, client, ethClient, prices.NewNoOpGetter()) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, client, ethClient) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) @@ -105,7 +104,7 @@ 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.TestLogger(t), &arbConfig{}, client, ethClient, prices.NewNoOpGetter()) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, client, ethClient) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) @@ -130,7 +129,7 @@ func TestArbitrumEstimator(t *testing.T) { t.Run("calling BumpLegacyGas always returns error", func(t *testing.T) { rpcClient := mocks.NewRPCClient(t) ethClient := mocks.NewETHClient(t) - o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, rpcClient, ethClient, prices.NewNoOpGetter()) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, rpcClient, ethClient) _, _, err := o.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), gasLimit, assets.NewWeiI(10), nil) assert.EqualError(t, err, "bump gas is not supported for this l2") }) @@ -138,7 +137,7 @@ func TestArbitrumEstimator(t *testing.T) { 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.TestLogger(t), &arbConfig{}, client, ethClient, prices.NewNoOpGetter()) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{}, client, ethClient) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(errors.New("kaboom")) ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { @@ -181,7 +180,7 @@ func TestArbitrumEstimator(t *testing.T) { assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(b.Bytes(), nil) - o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient, prices.NewNoOpGetter()) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient) require.NoError(t, o.Start(testutils.Context(t))) t.Cleanup(func() { assert.NoError(t, o.Close()) }) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) @@ -216,7 +215,7 @@ func TestArbitrumEstimator(t *testing.T) { assert.Equal(t, big.NewInt(-1), blockNumber) }).Return(b.Bytes(), nil) - o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient, prices.NewNoOpGetter()) + o := gas.NewArbitrumEstimator(logger.TestLogger(t), &arbConfig{v: maxGasLimit}, rpcClient, ethClient) require.NoError(t, o.Start(testutils.Context(t))) t.Cleanup(func() { assert.NoError(t, o.Close()) }) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) diff --git a/core/chains/evm/gas/block_history_estimator.go b/core/chains/evm/gas/block_history_estimator.go index 370738103f9..556c0b1715c 100644 --- a/core/chains/evm/gas/block_history_estimator.go +++ b/core/chains/evm/gas/block_history_estimator.go @@ -119,15 +119,14 @@ type ( latestMu sync.RWMutex initialFetch atomic.Bool - logger logger.SugaredLogger - priceComponentGetter PriceComponentGetter + logger logger.SugaredLogger } ) // 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, p PriceComponentGetter) EvmEstimator { +func NewBlockHistoryEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg chainConfig, eCfg estimatorGasEstimatorConfig, bhCfg BlockHistoryConfig, chainID big.Int) EvmEstimator { ctx, cancel := context.WithCancel(context.Background()) b := &BlockHistoryEstimator{ ethClient: ethClient, @@ -137,13 +136,12 @@ func NewBlockHistoryEstimator(lggr logger.Logger, ethClient evmclient.Client, cf bhConfig: bhCfg, blocks: make([]evmtypes.Block, 0), // Must have enough blocks for both estimator and connectivity checker - size: int64(mathutil.Max(bhCfg.BlockHistorySize(), bhCfg.CheckInclusionBlocks())), - mb: utils.NewSingleMailbox[*evmtypes.Head](), - wg: new(sync.WaitGroup), - ctx: ctx, - ctxCancel: cancel, - logger: logger.Sugared(lggr.Named("BlockHistoryEstimator")), - priceComponentGetter: p, + size: int64(mathutil.Max(bhCfg.BlockHistorySize(), bhCfg.CheckInclusionBlocks())), + mb: utils.NewSingleMailbox[*evmtypes.Head](), + wg: new(sync.WaitGroup), + ctx: ctx, + ctxCancel: cancel, + logger: logger.Sugared(lggr.Named("BlockHistoryEstimator")), } return b @@ -248,14 +246,6 @@ func (b *BlockHistoryEstimator) HealthReport() map[string]error { return map[string]error{b.Name(): b.StartStopOnce.Healthy()} } -func (b *BlockHistoryEstimator) GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) { - gasPrice, _, err := b.GetLegacyGas(ctx, nil, 0, maxGasPriceWei, opts...) - if err != nil { - return []PriceComponent{}, err - } - return b.priceComponentGetter.GetPriceComponents(ctx, gasPrice) -} - func (b *BlockHistoryEstimator) GetLegacyGas(_ context.Context, _ []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, _ ...feetypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { ok := b.IfStarted(func() { gasPrice = b.getGasPrice() @@ -510,11 +500,6 @@ func (b *BlockHistoryEstimator) FetchBlocksAndRecalculate(ctx context.Context, h } b.initialFetch.Store(true) b.Recalculate(head) - - if err := b.priceComponentGetter.RefreshComponents(b.ctx); err != nil { - b.logger.Warnw("Error refreshing price components", "err", err) - return - } } // Recalculate adds the given heads to the history and recalculates gas price. diff --git a/core/chains/evm/gas/block_history_estimator_test.go b/core/chains/evm/gas/block_history_estimator_test.go index e867c439fd9..c44e68002d2 100644 --- a/core/chains/evm/gas/block_history_estimator_test.go +++ b/core/chains/evm/gas/block_history_estimator_test.go @@ -22,7 +22,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/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/prices" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" @@ -42,7 +41,7 @@ func newBlockHistoryConfig() *gas.MockBlockHistoryConfig { } 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.TestLogger(t), c, cfg, gCfg, bhCfg, cid, prices.NewNoOpGetter()) + return gas.NewBlockHistoryEstimator(logger.TestLogger(t), c, cfg, gCfg, bhCfg, cid) } func newBlockHistoryEstimator(t *testing.T, c evmclient.Client, cfg gas.Config, gCfg gas.GasEstimatorConfig, bhCfg gas.BlockHistoryConfig) *gas.BlockHistoryEstimator { @@ -2020,7 +2019,7 @@ func TestBlockHistoryEstimator_CheckConnectivity(t *testing.T) { geCfg.EIP1559DynamicFeesF = false bhe := gas.BlockHistoryEstimatorFromInterface( - gas.NewBlockHistoryEstimator(lggr, nil, cfg, geCfg, bhCfg, *testutils.NewRandomEVMChainID(), prices.NewNoOpGetter()), + gas.NewBlockHistoryEstimator(lggr, nil, cfg, geCfg, bhCfg, *testutils.NewRandomEVMChainID()), ) attempts := []gas.EvmPriorAttempt{ diff --git a/core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go b/core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go new file mode 100644 index 00000000000..6ea253a3a10 --- /dev/null +++ b/core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go @@ -0,0 +1,155 @@ +package chainoracles + +import ( + "context" + "fmt" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/assets" + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +type OracleType string + +const ( + Arbitrum OracleType = "ARBITRUM" + OPStack OracleType = "OP_STACK" +) + +// Reads L2-specific precompiles and caches the l1BaseFee set by the L2. +type l1BaseFeeOracle struct { + client evmclient.Client + pollPeriod time.Duration + logger logger.Logger + address string + selector string + + l1BaseFeeMu sync.RWMutex + l1BaseFee *assets.Wei + + chInitialised chan struct{} + chStop utils.StopChan + chDone chan struct{} + utils.StartStopOnce +} + +const ( + // ArbGasInfoAddress is the address of the "Precompiled contract that exists in every Arbitrum chain." + // https://github.com/OffchainLabs/nitro/blob/f7645453cfc77bf3e3644ea1ac031eff629df325/contracts/src/precompiles/ArbGasInfo.sol + ArbGasInfoAddress = "0x000000000000000000000000000000000000006C" + // ArbGasInfo_getL1BaseFeeEstimate is the a hex encoded call to: + // `function getL1BaseFeeEstimate() external view returns (uint256);` + ArbGasInfo_getL1BaseFeeEstimate = "f5d6ded7" + + // GasOracleAddress is the address of the "Precompiled contract that exists in every OP stack chain." + OPGasOracleAddress = "0x420000000000000000000000000000000000000F" + // GasOracle_l1BaseFee is the a hex encoded call to: + // `function l1BaseFee() external view returns (uint256);` + OPGasOracle_getL1BaseFeeEstimate = "519b4bd3" + + // Interval at which to poll for L1BaseFee. A good starting point is the L1 block time. + PollPeriod = 12 * time.Second +) + +func NewL1BaeFeeOracle(lggr logger.Logger, ethClient evmclient.Client, oracleType OracleType) L1Oracle { + var address, selector string + switch oracleType { + case ARBITRUM: + address = ArbGasInfoAddress + selector = ArbGasInfo_getL1BaseFeeEstimate + case OP_STACK: + address = OPGasOracleAddress + selector = OPGasOracle_getL1BaseFeeEstimate + default: + panic(fmt.Errorf("unsupportd oracle type: %s", oracleType)) + } + + return &l1BaseFeeOracle{ + client: ethClient, + pollPeriod: PollPeriod, + logger: lggr.Named(fmt.Sprintf("%s L1BaseFeeOracle", oracleType)), + address: address, + selector: selector, + } +} + +func (o *l1BaseFeeOracle) Name() string { + return o.logger.Name() +} + +func (o *l1BaseFeeOracle) Start(ctx context.Context) error { + return o.StartOnce(o.Name(), func() error { + go o.run() + <-o.chInitialised + return nil + }) +} +func (o *l1BaseFeeOracle) Close() error { + return o.StopOnce(o.Name(), func() (err error) { + close(o.chStop) + <-o.chDone + return + }) +} + +func (o *l1BaseFeeOracle) Ready() error { return o.StartStopOnce.Ready() } + +func (o *l1BaseFeeOracle) HealthReport() map[string]error { + return map[string]error{o.Name(): o.StartStopOnce.Healthy()} +} + +func (o *l1BaseFeeOracle) 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 *l1BaseFeeOracle) refresh() (t *time.Timer) { + t = time.NewTimer(utils.WithJitter(o.pollPeriod)) + + ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + precompile := common.HexToAddress(o.address) + b, err := o.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: common.Hex2Bytes(o.selector), + }, big.NewInt(-1)) + if err != nil { + return + } + + if len(b) != 32 { // returns uint256; + o.logger.Warnf("return data length (%d) different than expected (%d)", len(b), 32) + return + } + price := new(big.Int).SetBytes(b) + + o.l1BaseFeeMu.Lock() + defer o.l1BaseFeeMu.Unlock() + o.l1BaseFee = assets.NewWei(price) + return +} + +func (o *l1BaseFeeOracle) L1GasPrice(_ context.Context) (*assets.Wei, error) { + o.l1BaseFeeMu.RLock() + defer o.l1BaseFeeMu.RUnlock() + return o.l1BaseFee, nil +} diff --git a/core/chains/evm/gas/chainoracles/models.go b/core/chains/evm/gas/chainoracles/models.go new file mode 100644 index 00000000000..92186ea583d --- /dev/null +++ b/core/chains/evm/gas/chainoracles/models.go @@ -0,0 +1,16 @@ +package chainoracles + +import ( + "context" + + "github.com/smartcontractkit/chainlink/v2/core/assets" + "github.com/smartcontractkit/chainlink/v2/core/services" +) + +// 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 +type L1Oracle interface { + services.ServiceCtx + + L1GasPrice(ctx context.Context) (*assets.Wei, error) +} diff --git a/core/chains/evm/gas/fixed_price_estimator.go b/core/chains/evm/gas/fixed_price_estimator.go index 029ea377bf9..a45df741a27 100644 --- a/core/chains/evm/gas/fixed_price_estimator.go +++ b/core/chains/evm/gas/fixed_price_estimator.go @@ -15,10 +15,9 @@ import ( var _ EvmEstimator = (*fixedPriceEstimator)(nil) type fixedPriceEstimator struct { - config fixedPriceEstimatorConfig - bhConfig fixedPriceEstimatorBlockHistoryConfig - lggr logger.SugaredLogger - priceComponentGetter PriceComponentGetter + config fixedPriceEstimatorConfig + bhConfig fixedPriceEstimatorBlockHistoryConfig + lggr logger.SugaredLogger } type bumpConfig interface { LimitMultiplier() float32 @@ -45,8 +44,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, p PriceComponentGetter) EvmEstimator { - return &fixedPriceEstimator{cfg, bhCfg, logger.Sugared(lggr.Named("FixedPriceEstimator")), p} +func NewFixedPriceEstimator(cfg fixedPriceEstimatorConfig, bhCfg fixedPriceEstimatorBlockHistoryConfig, lggr logger.Logger) EvmEstimator { + return &fixedPriceEstimator{cfg, bhCfg, logger.Sugared(lggr.Named("FixedPriceEstimator"))} } func (f *fixedPriceEstimator) Start(context.Context) error { @@ -59,11 +58,6 @@ func (f *fixedPriceEstimator) Start(context.Context) error { return nil } -func (f *fixedPriceEstimator) GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, _ ...feetypes.Opt) (prices []PriceComponent, err error) { - gasPrice := commonfee.CalculateFee(f.config.PriceDefault().ToInt(), maxGasPriceWei.ToInt(), f.config.PriceMax().ToInt()) - return f.priceComponentGetter.GetPriceComponents(ctx, assets.NewWei(gasPrice)) -} - func (f *fixedPriceEstimator) GetLegacyGas(_ context.Context, _ []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, _ ...feetypes.Opt) (*assets.Wei, uint32, error) { gasPrice := commonfee.CalculateFee(f.config.PriceDefault().ToInt(), maxGasPriceWei.ToInt(), f.config.PriceMax().ToInt()) chainSpecificGasLimit, err := commonfee.ApplyMultiplier(gasLimit, f.config.LimitMultiplier()) diff --git a/core/chains/evm/gas/fixed_price_estimator_test.go b/core/chains/evm/gas/fixed_price_estimator_test.go index da50a3eb816..a6cb313e5e2 100644 --- a/core/chains/evm/gas/fixed_price_estimator_test.go +++ b/core/chains/evm/gas/fixed_price_estimator_test.go @@ -8,7 +8,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/prices" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -27,7 +26,7 @@ func Test_FixedPriceEstimator(t *testing.T) { t.Run("GetLegacyGas returns EvmGasPriceDefault from config, with multiplier applied", func(t *testing.T) { config := &gas.MockGasEstimatorConfig{} - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t), prices.NewNoOpGetter()) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t)) config.PriceDefaultF = assets.NewWeiI(42) config.LimitMultiplierF = float32(1.1) @@ -44,7 +43,7 @@ func Test_FixedPriceEstimator(t *testing.T) { config.PriceDefaultF = assets.NewWeiI(42) config.LimitMultiplierF = float32(1.1) config.PriceMaxF = assets.NewWeiI(35) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t), prices.NewNoOpGetter()) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t)) gasPrice, gasLimit, err := f.GetLegacyGas(testutils.Context(t), nil, 100000, assets.NewWeiI(30)) require.NoError(t, err) @@ -58,7 +57,7 @@ func Test_FixedPriceEstimator(t *testing.T) { config.LimitMultiplierF = float32(1.1) config.PriceMaxF = assets.NewWeiI(20) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t), prices.NewNoOpGetter()) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, logger.TestLogger(t)) gasPrice, gasLimit, err := f.GetLegacyGas(testutils.Context(t), nil, 100000, assets.NewWeiI(30)) require.NoError(t, err) assert.Equal(t, 110000, int(gasLimit)) @@ -74,7 +73,7 @@ func Test_FixedPriceEstimator(t *testing.T) { config.BumpMinF = assets.NewWeiI(150) lggr := logger.TestLogger(t) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr, prices.NewNoOpGetter()) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr) gasPrice, gasLimit, err := f.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), 100000, maxGasPrice, nil) require.NoError(t, err) @@ -95,7 +94,7 @@ func Test_FixedPriceEstimator(t *testing.T) { config.BumpThresholdF = uint64(3) lggr := logger.TestLogger(t) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr, prices.NewNoOpGetter()) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr) fee, gasLimit, err := f.GetDynamicFee(testutils.Context(t), 100000, maxGasPrice) require.NoError(t, err) @@ -132,7 +131,7 @@ func Test_FixedPriceEstimator(t *testing.T) { config.BumpPercentF = uint16(10) lggr := logger.TestLogger(t) - f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr, prices.NewNoOpGetter()) + f := gas.NewFixedPriceEstimator(config, &blockHistoryConfig{}, lggr) originalFee := gas.DynamicFee{FeeCap: assets.NewWeiI(100), TipCap: assets.NewWeiI(25)} fee, gasLimit, err := f.BumpDynamicFee(testutils.Context(t), originalFee, 100000, maxGasPrice, nil) diff --git a/core/chains/evm/gas/l2_suggested_estimator.go b/core/chains/evm/gas/l2_suggested_estimator.go index cd719df287b..e59eca2b064 100644 --- a/core/chains/evm/gas/l2_suggested_estimator.go +++ b/core/chains/evm/gas/l2_suggested_estimator.go @@ -41,21 +41,18 @@ type l2SuggestedPriceEstimator struct { chInitialised chan struct{} chStop utils.StopChan chDone chan struct{} - - priceComponentGetter PriceComponentGetter } // NewL2SuggestedPriceEstimator returns a new Estimator which uses the L2 suggested gas price. -func NewL2SuggestedPriceEstimator(lggr logger.Logger, client rpcClient, p PriceComponentGetter) EvmEstimator { +func NewL2SuggestedPriceEstimator(lggr logger.Logger, client rpcClient) EvmEstimator { return &l2SuggestedPriceEstimator{ - client: client, - pollPeriod: 10 * time.Second, - logger: lggr.Named("L2SuggestedEstimator"), - chForceRefetch: make(chan (chan struct{})), - chInitialised: make(chan struct{}), - chStop: make(chan struct{}), - chDone: make(chan struct{}), - priceComponentGetter: p, + client: client, + pollPeriod: 10 * time.Second, + logger: lggr.Named("L2SuggestedEstimator"), + chForceRefetch: make(chan (chan struct{})), + chInitialised: make(chan struct{}), + chStop: make(chan struct{}), + chDone: make(chan struct{}), } } @@ -120,14 +117,6 @@ func (o *l2SuggestedPriceEstimator) refreshPrice() (t *time.Timer) { o.gasPriceMu.Lock() defer o.gasPriceMu.Unlock() o.l2GasPrice = bi - - priceGetterCtx, priceGetterCancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) - defer priceGetterCancel() - if err := o.priceComponentGetter.RefreshComponents(priceGetterCtx); err != nil { - o.logger.Warnf("Failed to refresh price component getter, got error: %s", err) - return - } - return } @@ -143,14 +132,6 @@ func (*l2SuggestedPriceEstimator) BumpDynamicFee(_ context.Context, _ DynamicFee return } -func (f *l2SuggestedPriceEstimator) GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) { - gasPrice, _, err := f.GetLegacyGas(ctx, nil, 0, maxGasPriceWei, opts...) - if err != nil { - return []PriceComponent{}, err - } - return f.priceComponentGetter.GetPriceComponents(ctx, gasPrice) -} - func (o *l2SuggestedPriceEstimator) GetLegacyGas(ctx context.Context, _ []byte, l2GasLimit uint32, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) { chainSpecificGasLimit = l2GasLimit diff --git a/core/chains/evm/gas/l2_suggested_estimator_test.go b/core/chains/evm/gas/l2_suggested_estimator_test.go index 2a9e8188e9b..69b36033024 100644 --- a/core/chains/evm/gas/l2_suggested_estimator_test.go +++ b/core/chains/evm/gas/l2_suggested_estimator_test.go @@ -13,7 +13,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/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/prices" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -28,7 +27,7 @@ func TestL2SuggestedEstimator(t *testing.T) { t.Run("calling GetLegacyGas on unstarted estimator returns error", func(t *testing.T) { client := mocks.NewRPCClient(t) - o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) _, _, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) assert.EqualError(t, err, "estimator is not started") }) @@ -40,7 +39,7 @@ func TestL2SuggestedEstimator(t *testing.T) { (*big.Int)(res).SetInt64(42) }) - o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) require.NoError(t, o.Start(testutils.Context(t))) t.Cleanup(func() { assert.NoError(t, o.Close()) }) gasPrice, chainSpecificGasLimit, err := o.GetLegacyGas(testutils.Context(t), calldata, gasLimit, maxGasPrice) @@ -51,7 +50,7 @@ func TestL2SuggestedEstimator(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.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) @@ -69,7 +68,7 @@ func TestL2SuggestedEstimator(t *testing.T) { t.Run("gas price is lower than global max gas price", func(t *testing.T) { client := mocks.NewRPCClient(t) - o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(nil).Run(func(args mock.Arguments) { res := args.Get(1).(*hexutil.Big) @@ -86,14 +85,14 @@ func TestL2SuggestedEstimator(t *testing.T) { t.Run("calling BumpLegacyGas always returns error", func(t *testing.T) { client := mocks.NewRPCClient(t) - o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) _, _, err := o.BumpLegacyGas(testutils.Context(t), assets.NewWeiI(42), gasLimit, assets.NewWeiI(10), nil) assert.EqualError(t, err, "bump gas is not supported for this l2") }) t.Run("calling GetLegacyGas on started estimator if initial call failed returns error", func(t *testing.T) { client := mocks.NewRPCClient(t) - o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client, prices.NewNoOpGetter()) + o := gas.NewL2SuggestedPriceEstimator(logger.TestLogger(t), client) client.On("CallContext", mock.Anything, mock.Anything, "eth_gasPrice").Return(errors.New("kaboom")) diff --git a/core/chains/evm/gas/mocks/evm_fee_estimator.go b/core/chains/evm/gas/mocks/evm_fee_estimator.go index 89fd8ecc71e..78317bc7f9f 100644 --- a/core/chains/evm/gas/mocks/evm_fee_estimator.go +++ b/core/chains/evm/gas/mocks/evm_fee_estimator.go @@ -139,49 +139,32 @@ func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, ca return r0, r1 } -// GetPrices provides a mock function with given fields: ctx, maxGasPriceWei, opts -func (_m *EvmFeeEstimator) GetPrices(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...types.Opt) ([]gas.PriceComponent, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, maxGasPriceWei) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) +// HealthReport provides a mock function with given fields: +func (_m *EvmFeeEstimator) HealthReport() map[string]error { + ret := _m.Called() - var r0 []gas.PriceComponent - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *assets.Wei, ...types.Opt) ([]gas.PriceComponent, error)); ok { - return rf(ctx, maxGasPriceWei, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *assets.Wei, ...types.Opt) []gas.PriceComponent); ok { - r0 = rf(ctx, maxGasPriceWei, opts...) + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]gas.PriceComponent) + r0 = ret.Get(0).(map[string]error) } } - if rf, ok := ret.Get(1).(func(context.Context, *assets.Wei, ...types.Opt) error); ok { - r1 = rf(ctx, maxGasPriceWei, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } -// HealthReport provides a mock function with given fields: -func (_m *EvmFeeEstimator) HealthReport() map[string]error { +// L1Oracle provides a mock function with given fields: +func (_m *EvmFeeEstimator) L1Oracle() gas.L1Oracle { ret := _m.Called() - var r0 map[string]error - if rf, ok := ret.Get(0).(func() map[string]error); ok { + var r0 gas.L1Oracle + if rf, ok := ret.Get(0).(func() gas.L1Oracle); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]error) + r0 = ret.Get(0).(gas.L1Oracle) } } diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index c3b44679e44..02061609ca2 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -15,12 +15,13 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/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/prices" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/chainoracles" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/label" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/config" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services" + "github.com/smartcontractkit/chainlink/v2/core/utils" bigmath "github.com/smartcontractkit/chainlink/v2/core/utils/big_math" ) @@ -31,7 +32,8 @@ type EvmFeeEstimator interface { services.ServiceCtx commontypes.HeadTrackable[*evmtypes.Head, common.Hash] - GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) + // L1Oracle returns the L1 gas price oracle only if the chain has one, e.g. OP stack L2s and Arbitrum. + L1Oracle() chainoracles.L1Oracle GetFee(ctx context.Context, calldata []byte, feeLimit uint32, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint32, err error) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint32, maxFeePrice *assets.Wei, attempts []EvmPriorAttempt) (bumpedFee EvmFee, chainSpecificFeeLimit uint32, err error) @@ -65,20 +67,20 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, ge df := geCfg.EIP1559DynamicFees() switch s { case "Arbitrum": - p := prices.NewGasOracleGetter(lggr, ethClient, prices.ARBITRUM, L1_BASE_FEE) - return NewWrappedEvmEstimator(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient, p), df) + l1Oracle := chainoracles.NewL1BaeFeeOracle(lggr, ethClient, chainoracles.Arbitrum) + return NewWrappedEvmEstimatorWithL1Oracle(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient), df, l1Oracle) case "BlockHistory": - return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID(), prices.NewNoOpGetter()), df) + return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()), df) case "OPStack": - p := prices.NewGasOracleGetter(lggr, ethClient, prices.OP_STACK, L1_BASE_FEE) - return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID(), p), df) + l1Oracle := chainoracles.NewL1BaeFeeOracle(lggr, ethClient, chainoracles.OPStack) + return NewWrappedEvmEstimatorWithL1Oracle(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()), df, l1Oracle) case "FixedPrice": - return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr, prices.NewNoOpGetter()), df) + return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df) case "Optimism2", "L2Suggested": - return NewWrappedEvmEstimator(NewL2SuggestedPriceEstimator(lggr, ethClient, prices.NewNoOpGetter()), df) + return NewWrappedEvmEstimator(NewL2SuggestedPriceEstimator(lggr, ethClient), df) default: lggr.Warnf("GasEstimator: unrecognised mode '%s', falling back to FixedPriceEstimator", s) - return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr, prices.NewNoOpGetter()), df) + return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df) } } @@ -104,8 +106,6 @@ type EvmEstimator interface { commontypes.HeadTrackable[*evmtypes.Head, common.Hash] services.ServiceCtx - // GetPriceComponents returns chain-specific price components - GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) // GetLegacyGas Calculates initial gas fee for non-EIP1559 transaction // maxGasPriceWei parameter is the highest possible gas fee cap that the function will return GetLegacyGas(ctx context.Context, calldata []byte, gasLimit uint32, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint32, err error) @@ -149,6 +149,8 @@ func (fee EvmFee) ValidDynamic() bool { type WrappedEvmEstimator struct { EvmEstimator EIP1559Enabled bool + l1Oracle chainoracles.L1Oracle + utils.StartStopOnce } var _ EvmFeeEstimator = (*WrappedEvmEstimator)(nil) @@ -157,14 +159,88 @@ func NewWrappedEvmEstimator(e EvmEstimator, eip1559Enabled bool) EvmFeeEstimator return &WrappedEvmEstimator{ EvmEstimator: e, EIP1559Enabled: eip1559Enabled, + l1Oracle: nil, } } -func (e WrappedEvmEstimator) GetPriceComponents(ctx context.Context, maxGasPriceWei *assets.Wei, opts ...feetypes.Opt) (prices []PriceComponent, err error) { - return e.EvmEstimator.GetPriceComponents(ctx, maxGasPriceWei, opts...) +func NewWrappedEvmEstimatorWithL1Oracle(e EvmEstimator, eip1559Enabled bool, o chainoracles.L1Oracle) EvmFeeEstimator { + return &WrappedEvmEstimator{ + EvmEstimator: e, + EIP1559Enabled: eip1559Enabled, + l1Oracle: o, + } +} + +func (e *WrappedEvmEstimator) Name() string { + return fmt.Sprintf("WrappedEvmEstimator(%s)", e.EvmEstimator.Name()) +} + +func (e *WrappedEvmEstimator) Start(ctx context.Context) error { + return e.StartOnce(e.Name(), func() error { + if err := e.EvmEstimator.Start(ctx); err != nil { + return errors.Wrap(err, "failed to start EVMEstimator") + } + if e.l1Oracle != nil { + if err := e.l1Oracle.Start(ctx); err != nil { + return errors.Wrap(err, "failed to start L1Oracle") + } + } + return nil + }) +} +func (e *WrappedEvmEstimator) Close() error { + return e.StopOnce(e.Name(), func() error { + var errEVM, errOracle error + + errEVM = errors.Wrap(e.EvmEstimator.Close(), "failed to stop EVMEstimator") + if e.l1Oracle != nil { + errOracle = errors.Wrap(e.l1Oracle.Close(), "failed to stop L1Oracle") + } + + if errEVM != nil { + return errEVM + } + return errOracle + }) +} + +func (e *WrappedEvmEstimator) Ready() error { + var errEVM, errOracle error + + errEVM = e.EvmEstimator.Ready() + if e.l1Oracle != nil { + errOracle = e.l1Oracle.Ready() + } + + if errEVM != nil { + return errEVM + } + return errOracle +} + +func (e *WrappedEvmEstimator) HealthReport() map[string]error { + hr := make(map[string]error) + + evmReport := e.EvmEstimator.HealthReport() + for k, v := range evmReport { + hr[k] = v + } + if e.l1Oracle != nil { + l1Report := e.l1Oracle.HealthReport() + for k, v := range l1Report { + hr[k] = v + } + } + + hr[e.Name()] = e.StartStopOnce.Healthy() + return hr +} + +func (e *WrappedEvmEstimator) L1Oracle() chainoracles.L1Oracle { + return e.l1Oracle } -func (e WrappedEvmEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint32, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint32, err error) { +func (e *WrappedEvmEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint32, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint32, err error) { // get dynamic fee if e.EIP1559Enabled { var dynamicFee DynamicFee @@ -179,7 +255,7 @@ func (e WrappedEvmEstimator) GetFee(ctx context.Context, calldata []byte, feeLim return } -func (e WrappedEvmEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint32, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (*big.Int, error) { +func (e *WrappedEvmEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint32, 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 @@ -197,7 +273,7 @@ func (e WrappedEvmEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, return amountWithFees, nil } -func (e WrappedEvmEstimator) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint32, maxFeePrice *assets.Wei, attempts []EvmPriorAttempt) (bumpedFee EvmFee, chainSpecificFeeLimit uint32, err error) { +func (e *WrappedEvmEstimator) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint32, maxFeePrice *assets.Wei, attempts []EvmPriorAttempt) (bumpedFee EvmFee, chainSpecificFeeLimit uint32, err error) { // validate only 1 fee type is present if (!originalFee.ValidDynamic() && originalFee.Legacy == nil) || (originalFee.ValidDynamic() && originalFee.Legacy != nil) { err = errors.New("only one dynamic or legacy fee can be defined") diff --git a/core/chains/evm/gas/price_models.go b/core/chains/evm/gas/price_models.go deleted file mode 100644 index da7f921ea3b..00000000000 --- a/core/chains/evm/gas/price_models.go +++ /dev/null @@ -1,29 +0,0 @@ -package gas - -import ( - "context" - - "github.com/smartcontractkit/chainlink/v2/core/assets" -) - -type PriceType string - -const ( - GAS_PRICE PriceType = "GAS_PRICE" - L1_BASE_FEE PriceType = "L1_BASE_FEE" -) - -type PriceComponent struct { - Price *assets.Wei - PriceType PriceType -} - -// PriceComponentGetter provides interface for implementing chain-specific price components -// On L1 chains like Ethereum or Avax, the only component is the gas price. -// On Optimistic Rollups, there are two components - the L2 gas price, and L1 base fee for data availability. -// On future chains, there could be more or differing price components. -type PriceComponentGetter interface { - RefreshComponents(ctx context.Context) error - // GetPriceComponents The first component in prices should always be the passed-in gasPrice - GetPriceComponents(ctx context.Context, gasPrice *assets.Wei) (prices []PriceComponent, err error) -} diff --git a/core/chains/evm/gas/prices/gas_oracle_getter.go b/core/chains/evm/gas/prices/gas_oracle_getter.go deleted file mode 100644 index fd88108c596..00000000000 --- a/core/chains/evm/gas/prices/gas_oracle_getter.go +++ /dev/null @@ -1,108 +0,0 @@ -package prices - -import ( - "context" - "fmt" - "math/big" - "sync" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - - "github.com/smartcontractkit/chainlink/v2/core/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/logger" -) - -type OracleType string - -const ( - ARBITRUM OracleType = "ARBITRUM" - OP_STACK OracleType = "OP_STACK" -) - -type gasOracleGetter struct { - client evmclient.Client - logger logger.Logger - address string - selector string - componentPriceType gas.PriceType - - componentPriceMu sync.RWMutex - componentPrice *assets.Wei -} - -const ( - // ArbGasInfoAddress is the address of the "Precompiled contract that exists in every Arbitrum chain." - // https://github.com/OffchainLabs/nitro/blob/f7645453cfc77bf3e3644ea1ac031eff629df325/contracts/src/precompiles/ArbGasInfo.sol - ArbGasInfoAddress = "0x000000000000000000000000000000000000006C" - // ArbGasInfo_getL1BaseFeeEstimate is the a hex encoded call to: - // `function getL1BaseFeeEstimate() external view returns (uint256);` - ArbGasInfo_getL1BaseFeeEstimate = "f5d6ded7" - - // GasOracleAddress is the address of the "Precompiled contract that exists in every OP stack chain." - OPGasOracleAddress = "0x420000000000000000000000000000000000000F" - // GasOracle_l1BaseFee is the a hex encoded call to: - // `function l1BaseFee() external view returns (uint256);` - OPGasOracle_getL1BaseFeeEstimate = "519b4bd3" -) - -func NewGasOracleGetter(lggr logger.Logger, ethClient evmclient.Client, oracleType OracleType, componentPriceType gas.PriceType) gas.PriceComponentGetter { - var address, selector string - switch oracleType { - case ARBITRUM: - address = ArbGasInfoAddress - selector = ArbGasInfo_getL1BaseFeeEstimate - case OP_STACK: - address = OPGasOracleAddress - selector = OPGasOracle_getL1BaseFeeEstimate - default: - panic(fmt.Errorf("unsupportd oracle type: %s", oracleType)) - } - - return &gasOracleGetter{ - client: ethClient, - logger: lggr.Named("NewGasOracleGetter"), - address: address, - selector: selector, - componentPriceType: componentPriceType, - } -} - -func (o *gasOracleGetter) RefreshComponents(ctx context.Context) (err error) { - precompile := common.HexToAddress(o.address) - b, err := o.client.CallContract(ctx, ethereum.CallMsg{ - To: &precompile, - Data: common.Hex2Bytes(o.selector), - }, big.NewInt(-1)) - if err != nil { - return err - } - - if len(b) != 32 { // returns uint256; - return fmt.Errorf("return data length (%d) different than expected (%d)", len(b), 32) - } - price := new(big.Int).SetBytes(b) - - o.componentPriceMu.Lock() - defer o.componentPriceMu.Unlock() - o.componentPrice = assets.NewWei(price) - - return -} - -func (o *gasOracleGetter) GetPriceComponents(_ context.Context, gasPrice *assets.Wei) (prices []gas.PriceComponent, err error) { - o.componentPriceMu.RLock() - defer o.componentPriceMu.RUnlock() - return []gas.PriceComponent{ - { - Price: gasPrice, - PriceType: gas.GAS_PRICE, - }, - { - Price: o.componentPrice, - PriceType: o.componentPriceType, - }, - }, nil -} diff --git a/core/chains/evm/gas/prices/no_op_getter.go b/core/chains/evm/gas/prices/no_op_getter.go deleted file mode 100644 index f9e39b5d470..00000000000 --- a/core/chains/evm/gas/prices/no_op_getter.go +++ /dev/null @@ -1,27 +0,0 @@ -package prices - -import ( - "context" - - "github.com/smartcontractkit/chainlink/v2/core/assets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" -) - -type noOpGetter struct{} - -func NewNoOpGetter() gas.PriceComponentGetter { - return &noOpGetter{} -} - -func (e *noOpGetter) RefreshComponents(_ context.Context) error { - return nil -} - -func (e *noOpGetter) GetPriceComponents(_ context.Context, gasPrice *assets.Wei) (prices []gas.PriceComponent, err error) { - return []gas.PriceComponent{ - { - Price: gasPrice, - PriceType: gas.GAS_PRICE, - }, - }, nil -} From d6e8274b4e4c3330b5c910a94e3eb0d534ac2b63 Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Sun, 10 Sep 2023 00:07:42 -0400 Subject: [PATCH 04/14] nit fixes --- core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go b/core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go index 6ea253a3a10..8080550f512 100644 --- a/core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go +++ b/core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go @@ -52,7 +52,7 @@ const ( OPGasOracleAddress = "0x420000000000000000000000000000000000000F" // GasOracle_l1BaseFee is the a hex encoded call to: // `function l1BaseFee() external view returns (uint256);` - OPGasOracle_getL1BaseFeeEstimate = "519b4bd3" + OPGasOracle_l1BaseFee = "519b4bd3" // Interval at which to poll for L1BaseFee. A good starting point is the L1 block time. PollPeriod = 12 * time.Second @@ -61,12 +61,12 @@ const ( func NewL1BaeFeeOracle(lggr logger.Logger, ethClient evmclient.Client, oracleType OracleType) L1Oracle { var address, selector string switch oracleType { - case ARBITRUM: + case Arbitrum: address = ArbGasInfoAddress selector = ArbGasInfo_getL1BaseFeeEstimate - case OP_STACK: + case OPStack: address = OPGasOracleAddress - selector = OPGasOracle_getL1BaseFeeEstimate + selector = OPGasOracle_l1BaseFee default: panic(fmt.Errorf("unsupportd oracle type: %s", oracleType)) } From d5ab5810f719ba53d1d0231e1398fb96f5a1847b Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Sun, 10 Sep 2023 00:24:47 -0400 Subject: [PATCH 05/14] change L1BaseFee to L1GasPrice --- ...e_fee_oracle.go => l1_gas_price_oracle.go} | 42 +++++++++---------- core/chains/evm/gas/models.go | 4 +- 2 files changed, 23 insertions(+), 23 deletions(-) rename core/chains/evm/gas/chainoracles/{l1_base_fee_oracle.go => l1_gas_price_oracle.go} (75%) diff --git a/core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go similarity index 75% rename from core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go rename to core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go index 8080550f512..0f924aa3617 100644 --- a/core/chains/evm/gas/chainoracles/l1_base_fee_oracle.go +++ b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go @@ -23,16 +23,16 @@ const ( OPStack OracleType = "OP_STACK" ) -// Reads L2-specific precompiles and caches the l1BaseFee set by the L2. -type l1BaseFeeOracle struct { +// Reads L2-specific precompiles and caches the l1GasPrice set by the L2. +type l1GasPriceOracle struct { client evmclient.Client pollPeriod time.Duration logger logger.Logger address string selector string - l1BaseFeeMu sync.RWMutex - l1BaseFee *assets.Wei + l1GasPriceMu sync.RWMutex + l1GasPrice *assets.Wei chInitialised chan struct{} chStop utils.StopChan @@ -58,7 +58,7 @@ const ( PollPeriod = 12 * time.Second ) -func NewL1BaeFeeOracle(lggr logger.Logger, ethClient evmclient.Client, oracleType OracleType) L1Oracle { +func NewL1GasPriceOracle(lggr logger.Logger, ethClient evmclient.Client, oracleType OracleType) L1Oracle { var address, selector string switch oracleType { case Arbitrum: @@ -71,27 +71,27 @@ func NewL1BaeFeeOracle(lggr logger.Logger, ethClient evmclient.Client, oracleTyp panic(fmt.Errorf("unsupportd oracle type: %s", oracleType)) } - return &l1BaseFeeOracle{ + return &l1GasPriceOracle{ client: ethClient, pollPeriod: PollPeriod, - logger: lggr.Named(fmt.Sprintf("%s L1BaseFeeOracle", oracleType)), + logger: lggr.Named(fmt.Sprintf("%s L1GasPriceOracle", oracleType)), address: address, selector: selector, } } -func (o *l1BaseFeeOracle) Name() string { +func (o *l1GasPriceOracle) Name() string { return o.logger.Name() } -func (o *l1BaseFeeOracle) Start(ctx context.Context) error { +func (o *l1GasPriceOracle) Start(ctx context.Context) error { return o.StartOnce(o.Name(), func() error { go o.run() <-o.chInitialised return nil }) } -func (o *l1BaseFeeOracle) Close() error { +func (o *l1GasPriceOracle) Close() error { return o.StopOnce(o.Name(), func() (err error) { close(o.chStop) <-o.chDone @@ -99,13 +99,13 @@ func (o *l1BaseFeeOracle) Close() error { }) } -func (o *l1BaseFeeOracle) Ready() error { return o.StartStopOnce.Ready() } +func (o *l1GasPriceOracle) Ready() error { return o.StartStopOnce.Ready() } -func (o *l1BaseFeeOracle) HealthReport() map[string]error { +func (o *l1GasPriceOracle) HealthReport() map[string]error { return map[string]error{o.Name(): o.StartStopOnce.Healthy()} } -func (o *l1BaseFeeOracle) run() { +func (o *l1GasPriceOracle) run() { defer close(o.chDone) t := o.refresh() @@ -121,7 +121,7 @@ func (o *l1BaseFeeOracle) run() { } } -func (o *l1BaseFeeOracle) refresh() (t *time.Timer) { +func (o *l1GasPriceOracle) refresh() (t *time.Timer) { t = time.NewTimer(utils.WithJitter(o.pollPeriod)) ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) @@ -142,14 +142,14 @@ func (o *l1BaseFeeOracle) refresh() (t *time.Timer) { } price := new(big.Int).SetBytes(b) - o.l1BaseFeeMu.Lock() - defer o.l1BaseFeeMu.Unlock() - o.l1BaseFee = assets.NewWei(price) + o.l1GasPriceMu.Lock() + defer o.l1GasPriceMu.Unlock() + o.l1GasPrice = assets.NewWei(price) return } -func (o *l1BaseFeeOracle) L1GasPrice(_ context.Context) (*assets.Wei, error) { - o.l1BaseFeeMu.RLock() - defer o.l1BaseFeeMu.RUnlock() - return o.l1BaseFee, nil +func (o *l1GasPriceOracle) L1GasPrice(_ context.Context) (*assets.Wei, error) { + o.l1GasPriceMu.RLock() + defer o.l1GasPriceMu.RUnlock() + return o.l1GasPrice, nil } diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index 02061609ca2..d8118072e63 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -67,12 +67,12 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, ge df := geCfg.EIP1559DynamicFees() switch s { case "Arbitrum": - l1Oracle := chainoracles.NewL1BaeFeeOracle(lggr, ethClient, chainoracles.Arbitrum) + l1Oracle := chainoracles.NewL1GasPriceOracle(lggr, ethClient, chainoracles.Arbitrum) return NewWrappedEvmEstimatorWithL1Oracle(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient), df, l1Oracle) case "BlockHistory": return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()), df) case "OPStack": - l1Oracle := chainoracles.NewL1BaeFeeOracle(lggr, ethClient, chainoracles.OPStack) + l1Oracle := chainoracles.NewL1GasPriceOracle(lggr, ethClient, chainoracles.OPStack) return NewWrappedEvmEstimatorWithL1Oracle(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()), df, l1Oracle) case "FixedPrice": return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df) From 56efe1d1de72a7f23152440fb6114ec453ca273d Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Sun, 10 Sep 2023 00:37:01 -0400 Subject: [PATCH 06/14] update mock --- core/chains/evm/gas/mocks/evm_fee_estimator.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/chains/evm/gas/mocks/evm_fee_estimator.go b/core/chains/evm/gas/mocks/evm_fee_estimator.go index 78317bc7f9f..8fb0409154f 100644 --- a/core/chains/evm/gas/mocks/evm_fee_estimator.go +++ b/core/chains/evm/gas/mocks/evm_fee_estimator.go @@ -7,6 +7,8 @@ import ( assets "github.com/smartcontractkit/chainlink/v2/core/assets" + chainoracles "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/chainoracles" + context "context" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" @@ -156,15 +158,15 @@ func (_m *EvmFeeEstimator) HealthReport() map[string]error { } // L1Oracle provides a mock function with given fields: -func (_m *EvmFeeEstimator) L1Oracle() gas.L1Oracle { +func (_m *EvmFeeEstimator) L1Oracle() chainoracles.L1Oracle { ret := _m.Called() - var r0 gas.L1Oracle - if rf, ok := ret.Get(0).(func() gas.L1Oracle); ok { + var r0 chainoracles.L1Oracle + if rf, ok := ret.Get(0).(func() chainoracles.L1Oracle); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(gas.L1Oracle) + r0 = ret.Get(0).(chainoracles.L1Oracle) } } From 41f5bd4e306f43ffdf14ef3454e057d874478599 Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Wed, 13 Sep 2023 00:31:52 -0400 Subject: [PATCH 07/14] use chain type instead of modifying chain config --- .../evm/config/toml/defaults/Base_Goerli.toml | 1 - .../evm/config/toml/defaults/Base_Mainnet.toml | 1 - .../config/toml/defaults/Optimism_Goerli.toml | 1 - .../config/toml/defaults/Optimism_Mainnet.toml | 1 - .../gas/chainoracles/l1_gas_price_oracle.go | 18 +++++++----------- core/chains/evm/gas/models.go | 6 ++---- 6 files changed, 9 insertions(+), 19 deletions(-) diff --git a/core/chains/evm/config/toml/defaults/Base_Goerli.toml b/core/chains/evm/config/toml/defaults/Base_Goerli.toml index 092974b0aee..5ecfd036f46 100644 --- a/core/chains/evm/config/toml/defaults/Base_Goerli.toml +++ b/core/chains/evm/config/toml/defaults/Base_Goerli.toml @@ -6,7 +6,6 @@ NoNewHeadsThreshold = '40s' MinIncomingConfirmations = 1 [GasEstimator] -Mode = 'OPStack' EIP1559DynamicFees = true PriceMin = '1 wei' BumpMin = '100 wei' diff --git a/core/chains/evm/config/toml/defaults/Base_Mainnet.toml b/core/chains/evm/config/toml/defaults/Base_Mainnet.toml index b724b01db30..314c12f8c54 100644 --- a/core/chains/evm/config/toml/defaults/Base_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Base_Mainnet.toml @@ -6,7 +6,6 @@ NoNewHeadsThreshold = '40s' MinIncomingConfirmations = 1 [GasEstimator] -Mode = 'OPStack' EIP1559DynamicFees = true PriceMin = '1 wei' BumpMin = '100 wei' diff --git a/core/chains/evm/config/toml/defaults/Optimism_Goerli.toml b/core/chains/evm/config/toml/defaults/Optimism_Goerli.toml index 21e488576fe..458b3b08123 100644 --- a/core/chains/evm/config/toml/defaults/Optimism_Goerli.toml +++ b/core/chains/evm/config/toml/defaults/Optimism_Goerli.toml @@ -7,7 +7,6 @@ NoNewHeadsThreshold = '40s' MinIncomingConfirmations = 1 [GasEstimator] -Mode = 'OPStack' EIP1559DynamicFees = true PriceMin = '1 wei' BumpMin = '100 wei' diff --git a/core/chains/evm/config/toml/defaults/Optimism_Mainnet.toml b/core/chains/evm/config/toml/defaults/Optimism_Mainnet.toml index bff70e35da1..fd4dd9f32f0 100644 --- a/core/chains/evm/config/toml/defaults/Optimism_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Optimism_Mainnet.toml @@ -7,7 +7,6 @@ NoNewHeadsThreshold = '40s' MinIncomingConfirmations = 1 [GasEstimator] -Mode = 'OPStack' EIP1559DynamicFees = true PriceMin = '1 wei' BumpMin = '100 wei' diff --git a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go index 0f924aa3617..b4378d6588e 100644 --- a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go +++ b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go @@ -12,17 +12,13 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/assets" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/config" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/utils" ) type OracleType string -const ( - Arbitrum OracleType = "ARBITRUM" - OPStack OracleType = "OP_STACK" -) - // Reads L2-specific precompiles and caches the l1GasPrice set by the L2. type l1GasPriceOracle struct { client evmclient.Client @@ -58,23 +54,23 @@ const ( PollPeriod = 12 * time.Second ) -func NewL1GasPriceOracle(lggr logger.Logger, ethClient evmclient.Client, oracleType OracleType) L1Oracle { +func NewL1GasPriceOracle(lggr logger.Logger, ethClient evmclient.Client, chainType config.ChainType) L1Oracle { var address, selector string - switch oracleType { - case Arbitrum: + switch chainType { + case config.ChainArbitrum: address = ArbGasInfoAddress selector = ArbGasInfo_getL1BaseFeeEstimate - case OPStack: + case config.ChainOptimismBedrock: address = OPGasOracleAddress selector = OPGasOracle_l1BaseFee default: - panic(fmt.Errorf("unsupportd oracle type: %s", oracleType)) + return nil } return &l1GasPriceOracle{ client: ethClient, pollPeriod: PollPeriod, - logger: lggr.Named(fmt.Sprintf("%s L1GasPriceOracle", oracleType)), + logger: lggr.Named(fmt.Sprintf("%d L1GasPriceOracle", chainType)), address: address, selector: selector, } diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index d8118072e63..cf5e7d34e5a 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -67,12 +67,10 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, ge df := geCfg.EIP1559DynamicFees() switch s { case "Arbitrum": - l1Oracle := chainoracles.NewL1GasPriceOracle(lggr, ethClient, chainoracles.Arbitrum) + l1Oracle := chainoracles.NewL1GasPriceOracle(lggr, ethClient, cfg.ChainType()) return NewWrappedEvmEstimatorWithL1Oracle(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient), df, l1Oracle) case "BlockHistory": - return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()), df) - case "OPStack": - l1Oracle := chainoracles.NewL1GasPriceOracle(lggr, ethClient, chainoracles.OPStack) + l1Oracle := chainoracles.NewL1GasPriceOracle(lggr, ethClient, cfg.ChainType()) return NewWrappedEvmEstimatorWithL1Oracle(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()), df, l1Oracle) case "FixedPrice": return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df) From 055aeb1e206dbb8412604ebbbafea43e91bce356 Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Wed, 13 Sep 2023 02:10:01 -0400 Subject: [PATCH 08/14] add mocks --- .../evm/gas/chainoracles/mocks/l1_oracle.go | 129 ++++++++++++++++++ core/chains/evm/gas/chainoracles/models.go | 4 +- core/chains/evm/gas/models_test.go | 14 ++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 core/chains/evm/gas/chainoracles/mocks/l1_oracle.go diff --git a/core/chains/evm/gas/chainoracles/mocks/l1_oracle.go b/core/chains/evm/gas/chainoracles/mocks/l1_oracle.go new file mode 100644 index 00000000000..cd896370e81 --- /dev/null +++ b/core/chains/evm/gas/chainoracles/mocks/l1_oracle.go @@ -0,0 +1,129 @@ +// Code generated by mockery v2.28.1. DO NOT EDIT. + +package mocks + +import ( + assets "github.com/smartcontractkit/chainlink/v2/core/assets" + + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// L1Oracle is an autogenerated mock type for the L1Oracle type +type L1Oracle struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *L1Oracle) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// HealthReport provides a mock function with given fields: +func (_m *L1Oracle) HealthReport() map[string]error { + ret := _m.Called() + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// L1GasPrice provides a mock function with given fields: ctx +func (_m *L1Oracle) L1GasPrice(ctx context.Context) (*assets.Wei, error) { + ret := _m.Called(ctx) + + var r0 *assets.Wei + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*assets.Wei, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *assets.Wei); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*assets.Wei) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Name provides a mock function with given fields: +func (_m *L1Oracle) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Ready provides a mock function with given fields: +func (_m *L1Oracle) Ready() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Start provides a mock function with given fields: _a0 +func (_m *L1Oracle) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewL1Oracle interface { + mock.TestingT + Cleanup(func()) +} + +// NewL1Oracle creates a new instance of L1Oracle. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewL1Oracle(t mockConstructorTestingTNewL1Oracle) *L1Oracle { + mock := &L1Oracle{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/chains/evm/gas/chainoracles/models.go b/core/chains/evm/gas/chainoracles/models.go index 92186ea583d..e95fb036aaf 100644 --- a/core/chains/evm/gas/chainoracles/models.go +++ b/core/chains/evm/gas/chainoracles/models.go @@ -9,8 +9,10 @@ import ( // 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.ServiceCtx - + L1GasPrice(ctx context.Context) (*assets.Wei, error) } diff --git a/core/chains/evm/gas/models_test.go b/core/chains/evm/gas/models_test.go index 048646a980c..9f0a0a3de23 100644 --- a/core/chains/evm/gas/models_test.go +++ b/core/chains/evm/gas/models_test.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + oraclesMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/chainoracles/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" ) @@ -36,6 +37,19 @@ func TestWrappedEvmEstimator(t *testing.T) { e.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(legacyFee, gasLimit, nil).Once() + // L1Oracle returns the correct L1Oracle interface + t.Run("L1Oracle", func(t *testing.T) { + // expect nil + estimator := gas.NewWrappedEvmEstimator(e, false) + l1Oracle := estimator.L1Oracle() + assert.Nil(t, l1Oracle) + + o := oraclesMocks.NewL1Oracle(t) + estimator = gas.NewWrappedEvmEstimatorWithL1Oracle(e, false, o) + l1Oracle = estimator.L1Oracle() + assert.Equal(t, o, l1Oracle) + }) + // GetFee returns gas estimation based on configuration value t.Run("GetFee", func(t *testing.T) { // expect legacy fee data From 20272a488855124c5bc694e515a6364991d504b4 Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Wed, 13 Sep 2023 18:05:16 -0400 Subject: [PATCH 09/14] beef up models tests --- core/chains/evm/gas/models_test.go | 87 ++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/core/chains/evm/gas/models_test.go b/core/chains/evm/gas/models_test.go index 9f0a0a3de23..55ab858bf03 100644 --- a/core/chains/evm/gas/models_test.go +++ b/core/chains/evm/gas/models_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -37,6 +38,9 @@ func TestWrappedEvmEstimator(t *testing.T) { e.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(legacyFee, gasLimit, nil).Once() + mockEvmEstimatorName := "MockEstimator" + mockEstimatorName := "WrappedEvmEstimator(MockEstimator)" + // L1Oracle returns the correct L1Oracle interface t.Run("L1Oracle", func(t *testing.T) { // expect nil @@ -44,10 +48,11 @@ func TestWrappedEvmEstimator(t *testing.T) { l1Oracle := estimator.L1Oracle() assert.Nil(t, l1Oracle) - o := oraclesMocks.NewL1Oracle(t) - estimator = gas.NewWrappedEvmEstimatorWithL1Oracle(e, false, o) + // expect l1Oracle + oracle := oraclesMocks.NewL1Oracle(t) + estimator = gas.NewWrappedEvmEstimatorWithL1Oracle(e, false, oracle) l1Oracle = estimator.L1Oracle() - assert.Equal(t, o, l1Oracle) + assert.Equal(t, oracle, l1Oracle) }) // GetFee returns gas estimation based on configuration value @@ -127,4 +132,80 @@ func TestWrappedEvmEstimator(t *testing.T) { fee = new(big.Int).Mul(dynamicFee.FeeCap.ToInt(), big.NewInt(int64(gasLimit))) assert.Equal(t, new(big.Int).Add(val.ToInt(), fee), total) }) + + t.Run("Name", func(t *testing.T) { + evmEstimator := mocks.NewEvmEstimator(t) + oracle := oraclesMocks.NewL1Oracle(t) + + evmEstimator.On("Name").Return(mockEvmEstimatorName, nil).Once() + + estimator := gas.NewWrappedEvmEstimatorWithL1Oracle(evmEstimator, false, oracle) + name := estimator.Name() + require.Equal(t, mockEstimatorName, name) + }) + + t.Run("Start and stop calls both EVM estimator and L1Oracle", func(t *testing.T) { + evmEstimator := mocks.NewEvmEstimator(t) + oracle := oraclesMocks.NewL1Oracle(t) + + evmEstimator.On("Name").Return(mockEvmEstimatorName, nil).Times(4) + evmEstimator.On("Start", mock.Anything).Return(nil).Twice() + evmEstimator.On("Close").Return(nil).Twice() + oracle.On("Start", mock.Anything).Return(nil).Once() + oracle.On("Close").Return(nil).Once() + + estimator := gas.NewWrappedEvmEstimator(evmEstimator, false) + err := estimator.Start(ctx) + require.NoError(t, err) + err = estimator.Close() + require.NoError(t, err) + + estimator = gas.NewWrappedEvmEstimatorWithL1Oracle(evmEstimator, false, oracle) + err = estimator.Start(ctx) + require.NoError(t, err) + err = estimator.Close() + require.NoError(t, err) + }) + + t.Run("Read calls both EVM estimator and L1Oracle", func(t *testing.T) { + evmEstimator := mocks.NewEvmEstimator(t) + oracle := oraclesMocks.NewL1Oracle(t) + + evmEstimator.On("Ready").Return(nil).Twice() + oracle.On("Ready").Return(nil).Once() + + estimator := gas.NewWrappedEvmEstimator(evmEstimator, false) + err := estimator.Ready() + require.NoError(t, err) + + estimator = gas.NewWrappedEvmEstimatorWithL1Oracle(evmEstimator, false, oracle) + err = estimator.Ready() + require.NoError(t, err) + }) + + t.Run("HealthReport merges report from EVM estimator and L1Oracle", func(t *testing.T) { + evmEstimator := mocks.NewEvmEstimator(t) + oracle := oraclesMocks.NewL1Oracle(t) + + evmEstimatorKey := "evm" + evmEstimatorError := errors.New("evm error") + oracleKey := "oracle" + oracleError := errors.New("oracle error") + + evmEstimator.On("Name").Return(mockEvmEstimatorName, nil).Twice() + evmEstimator.On("HealthReport").Return(map[string]error{evmEstimatorKey: evmEstimatorError}).Twice() + oracle.On("HealthReport").Return(map[string]error{oracleKey: oracleError}).Once() + + estimator := gas.NewWrappedEvmEstimator(evmEstimator, false) + report := estimator.HealthReport() + require.True(t, errors.Is(report[evmEstimatorKey], evmEstimatorError)) + require.Nil(t, report[oracleKey]) + require.NotNil(t, report[mockEstimatorName]) + + estimator = gas.NewWrappedEvmEstimatorWithL1Oracle(evmEstimator, false, oracle) + report = estimator.HealthReport() + require.True(t, errors.Is(report[evmEstimatorKey], evmEstimatorError)) + require.True(t, errors.Is(report[oracleKey], oracleError)) + require.NotNil(t, report[mockEstimatorName]) + }) } From e5d129554b038c560d4fcd74cef5400d40129cb2 Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Thu, 14 Sep 2023 02:06:36 -0400 Subject: [PATCH 10/14] beef up l1 oracle tests --- .../gas/chainoracles/l1_gas_price_oracle.go | 47 +++++++---- .../chainoracles/l1_gas_price_oracle_test.go | 83 +++++++++++++++++++ .../evm/gas/chainoracles/mocks/eth_client.go | 59 +++++++++++++ core/chains/evm/gas/models.go | 17 ++-- 4 files changed, 179 insertions(+), 27 deletions(-) create mode 100644 core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go create mode 100644 core/chains/evm/gas/chainoracles/mocks/eth_client.go diff --git a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go index b4378d6588e..f8e1ea421bd 100644 --- a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go +++ b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" "github.com/smartcontractkit/chainlink/v2/core/assets" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" @@ -17,11 +18,14 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/utils" ) -type OracleType string +//go:generate mockery --quiet --name ethClient --output ./mocks/ --case=underscore --structname ETHClient +type ethClient interface { + CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) +} // Reads L2-specific precompiles and caches the l1GasPrice set by the L2. type l1GasPriceOracle struct { - client evmclient.Client + client ethClient pollPeriod time.Duration logger logger.Logger address string @@ -44,7 +48,8 @@ const ( // `function getL1BaseFeeEstimate() external view returns (uint256);` ArbGasInfo_getL1BaseFeeEstimate = "f5d6ded7" - // GasOracleAddress is the address of the "Precompiled contract that exists in every OP stack chain." + // GasOracleAddress is the address of the precompiled contract that exists on OP stack chain. + // This is the case for Optimism and Base OPGasOracleAddress = "0x420000000000000000000000000000000000000F" // GasOracle_l1BaseFee is the a hex encoded call to: // `function l1BaseFee() external view returns (uint256);` @@ -54,7 +59,7 @@ const ( PollPeriod = 12 * time.Second ) -func NewL1GasPriceOracle(lggr logger.Logger, ethClient evmclient.Client, chainType config.ChainType) L1Oracle { +func NewL1GasPriceOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType) L1Oracle { var address, selector string switch chainType { case config.ChainArbitrum: @@ -68,11 +73,14 @@ func NewL1GasPriceOracle(lggr logger.Logger, ethClient evmclient.Client, chainTy } return &l1GasPriceOracle{ - client: ethClient, - pollPeriod: PollPeriod, - logger: lggr.Named(fmt.Sprintf("%d L1GasPriceOracle", chainType)), - address: address, - selector: selector, + client: ethClient, + pollPeriod: PollPeriod, + logger: lggr.Named(fmt.Sprintf("L1GasPriceOracle(%s)", chainType)), + address: address, + selector: selector, + chInitialised: make(chan struct{}), + chStop: make(chan struct{}), + chDone: make(chan struct{}), } } @@ -88,10 +96,10 @@ func (o *l1GasPriceOracle) Start(ctx context.Context) error { }) } func (o *l1GasPriceOracle) Close() error { - return o.StopOnce(o.Name(), func() (err error) { + return o.StopOnce(o.Name(), func() error { close(o.chStop) <-o.chDone - return + return nil }) } @@ -144,8 +152,17 @@ func (o *l1GasPriceOracle) refresh() (t *time.Timer) { return } -func (o *l1GasPriceOracle) L1GasPrice(_ context.Context) (*assets.Wei, error) { - o.l1GasPriceMu.RLock() - defer o.l1GasPriceMu.RUnlock() - return o.l1GasPrice, nil +func (o *l1GasPriceOracle) L1GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) { + ok := o.IfStarted(func() { + o.l1GasPriceMu.RLock() + l1GasPrice = o.l1GasPrice + o.l1GasPriceMu.RUnlock() + }) + if !ok { + return l1GasPrice, errors.New("L1GasPriceOracle is not started; cannot estimate gas") + } + if l1GasPrice == nil { + return l1GasPrice, errors.New("failed to get l1 gas price; gas price not set") + } + return } diff --git a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go new file mode 100644 index 00000000000..5ceb8ea97ff --- /dev/null +++ b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go @@ -0,0 +1,83 @@ +package chainoracles + +import ( + "fmt" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/chainoracles/mocks" + "github.com/smartcontractkit/chainlink/v2/core/config" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func TestL1GasPriceOracle(t *testing.T) { + t.Parallel() + + t.Run("Unsupported ChainType returns nil", func(t *testing.T) { + ethClient := mocks.NewETHClient(t) + + oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainCelo) + assert.Nil(t, oracle) + }) + + t.Run("Calling L1GasPrice on unstarted L1Oracle returns error", func(t *testing.T) { + ethClient := mocks.NewETHClient(t) + + oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainOptimismBedrock) + + _, err := oracle.L1GasPrice(testutils.Context(t)) + assert.EqualError(t, err, "L1GasPriceOracle is not started; cannot estimate gas") + }) + + t.Run("Calling L1GasPrice on started Arbitrum L1Oracle returns Arbitrum l1GasPrice", func(t *testing.T) { + l1BaseFee := big.NewInt(100) + + ethClient := mocks.NewETHClient(t) + ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + callMsg := args.Get(1).(ethereum.CallMsg) + blockNumber := args.Get(2).(*big.Int) + assert.Equal(t, ArbGasInfoAddress, callMsg.To.String()) + assert.Equal(t, ArbGasInfo_getL1BaseFeeEstimate, fmt.Sprintf("%x", callMsg.Data)) + assert.Equal(t, big.NewInt(-1), blockNumber) + }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) + + oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainArbitrum) + require.NoError(t, oracle.Start(testutils.Context(t))) + t.Cleanup(func() { assert.NoError(t, oracle.Close()) }) + + gasPrice, err := oracle.L1GasPrice(testutils.Context(t)) + require.NoError(t, err) + + assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) + }) + + t.Run("Calling L1GasPrice on started OPStack L1Oracle returns OPStack l1GasPrice", func(t *testing.T) { + l1BaseFee := big.NewInt(200) + + ethClient := mocks.NewETHClient(t) + ethClient.On("CallContract", mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})).Run(func(args mock.Arguments) { + callMsg := args.Get(1).(ethereum.CallMsg) + blockNumber := args.Get(2).(*big.Int) + assert.Equal(t, OPGasOracleAddress, callMsg.To.String()) + assert.Equal(t, OPGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) + assert.Equal(t, big.NewInt(-1), blockNumber) + }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) + + oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainOptimismBedrock) + require.NoError(t, oracle.Start(testutils.Context(t))) + t.Cleanup(func() { assert.NoError(t, oracle.Close()) }) + + gasPrice, err := oracle.L1GasPrice(testutils.Context(t)) + require.NoError(t, err) + + assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) + }) +} diff --git a/core/chains/evm/gas/chainoracles/mocks/eth_client.go b/core/chains/evm/gas/chainoracles/mocks/eth_client.go new file mode 100644 index 00000000000..86228d4d1fe --- /dev/null +++ b/core/chains/evm/gas/chainoracles/mocks/eth_client.go @@ -0,0 +1,59 @@ +// Code generated by mockery v2.28.1. DO NOT EDIT. + +package mocks + +import ( + big "math/big" + + context "context" + + 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) + + 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 +} + +type mockConstructorTestingTNewETHClient interface { + mock.TestingT + Cleanup(func()) +} + +// 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. +func NewETHClient(t mockConstructorTestingTNewETHClient) *ETHClient { + mock := ÐClient{} + 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 cf5e7d34e5a..bb7e43d0d77 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" + "golang.org/x/exp/maps" commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" @@ -217,21 +218,13 @@ func (e *WrappedEvmEstimator) Ready() error { } func (e *WrappedEvmEstimator) HealthReport() map[string]error { - hr := make(map[string]error) - - evmReport := e.EvmEstimator.HealthReport() - for k, v := range evmReport { - hr[k] = v - } + report := map[string]error{e.Name(): e.StartStopOnce.Healthy()} + maps.Copy(report, e.EvmEstimator.HealthReport()) if e.l1Oracle != nil { - l1Report := e.l1Oracle.HealthReport() - for k, v := range l1Report { - hr[k] = v - } + maps.Copy(report, e.l1Oracle.HealthReport()) } - hr[e.Name()] = e.StartStopOnce.Healthy() - return hr + return report } func (e *WrappedEvmEstimator) L1Oracle() chainoracles.L1Oracle { From 008a45570700e7d08d487770d7ba1f8e5c1e00df Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Thu, 14 Sep 2023 02:11:45 -0400 Subject: [PATCH 11/14] small nits --- core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go index f8e1ea421bd..069a8dcc350 100644 --- a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go +++ b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go @@ -49,7 +49,7 @@ const ( ArbGasInfo_getL1BaseFeeEstimate = "f5d6ded7" // GasOracleAddress is the address of the precompiled contract that exists on OP stack chain. - // This is the case for Optimism and Base + // This is the case for Optimism and Base. OPGasOracleAddress = "0x420000000000000000000000000000000000000F" // GasOracle_l1BaseFee is the a hex encoded call to: // `function l1BaseFee() external view returns (uint256);` From 19282f2a8396f59caaa743aa5bf132b740c8cf8e Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Thu, 14 Sep 2023 16:58:13 -0400 Subject: [PATCH 12/14] address comments --- .../gas/chainoracles/l1_gas_price_oracle.go | 21 +++++++------- .../chainoracles/l1_gas_price_oracle_test.go | 7 ++--- core/chains/evm/gas/fixed_price_estimator.go | 1 - core/chains/evm/gas/models.go | 22 +++++---------- core/chains/evm/gas/models_test.go | 28 +++++++++---------- core/chains/evm/txmgr/broadcaster_test.go | 6 ++-- core/chains/evm/txmgr/confirmer_test.go | 6 ++-- core/internal/cltest/cltest.go | 2 +- 8 files changed, 42 insertions(+), 51 deletions(-) diff --git a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go index 069a8dcc350..1ab2b582097 100644 --- a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go +++ b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go @@ -2,6 +2,7 @@ package chainoracles import ( "context" + "errors" "fmt" "math/big" "sync" @@ -9,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" "github.com/smartcontractkit/chainlink/v2/core/assets" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" @@ -29,7 +29,7 @@ type l1GasPriceOracle struct { pollPeriod time.Duration logger logger.Logger address string - selector string + callArgs string l1GasPriceMu sync.RWMutex l1GasPrice *assets.Wei @@ -60,16 +60,16 @@ const ( ) func NewL1GasPriceOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType) L1Oracle { - var address, selector string + var address, callArgs string switch chainType { case config.ChainArbitrum: address = ArbGasInfoAddress - selector = ArbGasInfo_getL1BaseFeeEstimate + callArgs = ArbGasInfo_getL1BaseFeeEstimate case config.ChainOptimismBedrock: address = OPGasOracleAddress - selector = OPGasOracle_l1BaseFee + callArgs = OPGasOracle_l1BaseFee default: - return nil + panic(fmt.Sprintf("Received unspported chaintype %s", chainType)) } return &l1GasPriceOracle{ @@ -77,7 +77,7 @@ func NewL1GasPriceOracle(lggr logger.Logger, ethClient ethClient, chainType conf pollPeriod: PollPeriod, logger: lggr.Named(fmt.Sprintf("L1GasPriceOracle(%s)", chainType)), address: address, - selector: selector, + callArgs: callArgs, chInitialised: make(chan struct{}), chStop: make(chan struct{}), chDone: make(chan struct{}), @@ -134,14 +134,15 @@ func (o *l1GasPriceOracle) refresh() (t *time.Timer) { precompile := common.HexToAddress(o.address) b, err := o.client.CallContract(ctx, ethereum.CallMsg{ To: &precompile, - Data: common.Hex2Bytes(o.selector), - }, big.NewInt(-1)) + Data: common.Hex2Bytes(o.callArgs), + }, nil) if err != nil { + o.logger.Errorf("gas oracle contract call failed: %v", err) return } if len(b) != 32 { // returns uint256; - o.logger.Warnf("return data length (%d) different than expected (%d)", len(b), 32) + o.logger.Criticalf("return data length (%d) different than expected (%d)", len(b), 32) return } price := new(big.Int).SetBytes(b) diff --git a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go index 5ceb8ea97ff..f3d23de0867 100644 --- a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go +++ b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go @@ -24,8 +24,7 @@ func TestL1GasPriceOracle(t *testing.T) { t.Run("Unsupported ChainType returns nil", func(t *testing.T) { ethClient := mocks.NewETHClient(t) - oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainCelo) - assert.Nil(t, oracle) + assert.Panicsf(t, func() { NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainCelo) }, "Received unspported chaintype %s", config.ChainCelo) }) t.Run("Calling L1GasPrice on unstarted L1Oracle returns error", func(t *testing.T) { @@ -46,7 +45,7 @@ func TestL1GasPriceOracle(t *testing.T) { blockNumber := args.Get(2).(*big.Int) assert.Equal(t, ArbGasInfoAddress, callMsg.To.String()) assert.Equal(t, ArbGasInfo_getL1BaseFeeEstimate, fmt.Sprintf("%x", callMsg.Data)) - assert.Equal(t, big.NewInt(-1), blockNumber) + assert.Nil(t, blockNumber) }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainArbitrum) @@ -68,7 +67,7 @@ func TestL1GasPriceOracle(t *testing.T) { blockNumber := args.Get(2).(*big.Int) assert.Equal(t, OPGasOracleAddress, callMsg.To.String()) assert.Equal(t, OPGasOracle_l1BaseFee, fmt.Sprintf("%x", callMsg.Data)) - assert.Equal(t, big.NewInt(-1), blockNumber) + assert.Nil(t, blockNumber) }).Return(common.BigToHash(l1BaseFee).Bytes(), nil) oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainOptimismBedrock) diff --git a/core/chains/evm/gas/fixed_price_estimator.go b/core/chains/evm/gas/fixed_price_estimator.go index a45df741a27..733dd179fec 100644 --- a/core/chains/evm/gas/fixed_price_estimator.go +++ b/core/chains/evm/gas/fixed_price_estimator.go @@ -4,7 +4,6 @@ import ( "context" "github.com/pkg/errors" - commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" "github.com/smartcontractkit/chainlink/v2/core/assets" diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index bb7e43d0d77..f5d3ef934cf 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -69,17 +69,17 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, ge switch s { case "Arbitrum": l1Oracle := chainoracles.NewL1GasPriceOracle(lggr, ethClient, cfg.ChainType()) - return NewWrappedEvmEstimatorWithL1Oracle(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient), df, l1Oracle) + return NewWrappedEvmEstimator(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient), df, l1Oracle) case "BlockHistory": l1Oracle := chainoracles.NewL1GasPriceOracle(lggr, ethClient, cfg.ChainType()) - return NewWrappedEvmEstimatorWithL1Oracle(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()), df, l1Oracle) + return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()), df, l1Oracle) case "FixedPrice": - return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df) + return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df, nil) case "Optimism2", "L2Suggested": - return NewWrappedEvmEstimator(NewL2SuggestedPriceEstimator(lggr, ethClient), df) + return NewWrappedEvmEstimator(NewL2SuggestedPriceEstimator(lggr, ethClient), df, nil) default: lggr.Warnf("GasEstimator: unrecognised mode '%s', falling back to FixedPriceEstimator", s) - return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df) + return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df, nil) } } @@ -154,19 +154,11 @@ type WrappedEvmEstimator struct { var _ EvmFeeEstimator = (*WrappedEvmEstimator)(nil) -func NewWrappedEvmEstimator(e EvmEstimator, eip1559Enabled bool) EvmFeeEstimator { +func NewWrappedEvmEstimator(e EvmEstimator, eip1559Enabled bool, l1Oracle chainoracles.L1Oracle) EvmFeeEstimator { return &WrappedEvmEstimator{ EvmEstimator: e, EIP1559Enabled: eip1559Enabled, - l1Oracle: nil, - } -} - -func NewWrappedEvmEstimatorWithL1Oracle(e EvmEstimator, eip1559Enabled bool, o chainoracles.L1Oracle) EvmFeeEstimator { - return &WrappedEvmEstimator{ - EvmEstimator: e, - EIP1559Enabled: eip1559Enabled, - l1Oracle: o, + l1Oracle: l1Oracle, } } diff --git a/core/chains/evm/gas/models_test.go b/core/chains/evm/gas/models_test.go index 55ab858bf03..a4cac2db0cb 100644 --- a/core/chains/evm/gas/models_test.go +++ b/core/chains/evm/gas/models_test.go @@ -44,13 +44,13 @@ func TestWrappedEvmEstimator(t *testing.T) { // L1Oracle returns the correct L1Oracle interface t.Run("L1Oracle", func(t *testing.T) { // expect nil - estimator := gas.NewWrappedEvmEstimator(e, false) + estimator := gas.NewWrappedEvmEstimator(e, false, nil) l1Oracle := estimator.L1Oracle() assert.Nil(t, l1Oracle) // expect l1Oracle oracle := oraclesMocks.NewL1Oracle(t) - estimator = gas.NewWrappedEvmEstimatorWithL1Oracle(e, false, oracle) + estimator = gas.NewWrappedEvmEstimator(e, false, oracle) l1Oracle = estimator.L1Oracle() assert.Equal(t, oracle, l1Oracle) }) @@ -59,7 +59,7 @@ func TestWrappedEvmEstimator(t *testing.T) { t.Run("GetFee", func(t *testing.T) { // expect legacy fee data dynamicFees := false - estimator := gas.NewWrappedEvmEstimator(e, dynamicFees) + estimator := gas.NewWrappedEvmEstimator(e, dynamicFees, nil) fee, max, err := estimator.GetFee(ctx, nil, 0, nil) require.NoError(t, err) assert.Equal(t, gasLimit, max) @@ -69,7 +69,7 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect dynamic fee data dynamicFees = true - estimator = gas.NewWrappedEvmEstimator(e, dynamicFees) + estimator = gas.NewWrappedEvmEstimator(e, dynamicFees, nil) fee, max, err = estimator.GetFee(ctx, nil, 0, nil) require.NoError(t, err) assert.Equal(t, gasLimit, max) @@ -81,7 +81,7 @@ func TestWrappedEvmEstimator(t *testing.T) { // BumpFee returns bumped fee type based on original fee calculation t.Run("BumpFee", func(t *testing.T) { dynamicFees := false - estimator := gas.NewWrappedEvmEstimator(e, dynamicFees) + estimator := gas.NewWrappedEvmEstimator(e, dynamicFees, nil) // expect legacy fee data fee, max, err := estimator.BumpFee(ctx, gas.EvmFee{Legacy: assets.NewWeiI(0)}, 0, nil, nil) @@ -118,7 +118,7 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect legacy fee data dynamicFees := false - estimator := gas.NewWrappedEvmEstimator(e, dynamicFees) + estimator := gas.NewWrappedEvmEstimator(e, dynamicFees, nil) total, err := estimator.GetMaxCost(ctx, val, nil, gasLimit, nil) require.NoError(t, err) fee := new(big.Int).Mul(legacyFee.ToInt(), big.NewInt(int64(gasLimit))) @@ -126,7 +126,7 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect dynamic fee data dynamicFees = true - estimator = gas.NewWrappedEvmEstimator(e, dynamicFees) + estimator = gas.NewWrappedEvmEstimator(e, dynamicFees, nil) 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))) @@ -139,7 +139,7 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("Name").Return(mockEvmEstimatorName, nil).Once() - estimator := gas.NewWrappedEvmEstimatorWithL1Oracle(evmEstimator, false, oracle) + estimator := gas.NewWrappedEvmEstimator(evmEstimator, false, oracle) name := estimator.Name() require.Equal(t, mockEstimatorName, name) }) @@ -154,13 +154,13 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle.On("Start", mock.Anything).Return(nil).Once() oracle.On("Close").Return(nil).Once() - estimator := gas.NewWrappedEvmEstimator(evmEstimator, false) + estimator := gas.NewWrappedEvmEstimator(evmEstimator, false, nil) err := estimator.Start(ctx) require.NoError(t, err) err = estimator.Close() require.NoError(t, err) - estimator = gas.NewWrappedEvmEstimatorWithL1Oracle(evmEstimator, false, oracle) + estimator = gas.NewWrappedEvmEstimator(evmEstimator, false, oracle) err = estimator.Start(ctx) require.NoError(t, err) err = estimator.Close() @@ -174,11 +174,11 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("Ready").Return(nil).Twice() oracle.On("Ready").Return(nil).Once() - estimator := gas.NewWrappedEvmEstimator(evmEstimator, false) + estimator := gas.NewWrappedEvmEstimator(evmEstimator, false, nil) err := estimator.Ready() require.NoError(t, err) - estimator = gas.NewWrappedEvmEstimatorWithL1Oracle(evmEstimator, false, oracle) + estimator = gas.NewWrappedEvmEstimator(evmEstimator, false, oracle) err = estimator.Ready() require.NoError(t, err) }) @@ -196,13 +196,13 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("HealthReport").Return(map[string]error{evmEstimatorKey: evmEstimatorError}).Twice() oracle.On("HealthReport").Return(map[string]error{oracleKey: oracleError}).Once() - estimator := gas.NewWrappedEvmEstimator(evmEstimator, false) + estimator := gas.NewWrappedEvmEstimator(evmEstimator, false, nil) report := estimator.HealthReport() require.True(t, errors.Is(report[evmEstimatorKey], evmEstimatorError)) require.Nil(t, report[oracleKey]) require.NotNil(t, report[mockEstimatorName]) - estimator = gas.NewWrappedEvmEstimatorWithL1Oracle(evmEstimator, false, oracle) + estimator = gas.NewWrappedEvmEstimator(evmEstimator, false, oracle) report = estimator.HealthReport() require.True(t, errors.Is(report[evmEstimatorKey], evmEstimatorError)) require.True(t, errors.Is(report[oracleKey], oracleError)) diff --git a/core/chains/evm/txmgr/broadcaster_test.go b/core/chains/evm/txmgr/broadcaster_test.go index 2e665bc3c8f..649cbfd3bfd 100644 --- a/core/chains/evm/txmgr/broadcaster_test.go +++ b/core/chains/evm/txmgr/broadcaster_test.go @@ -67,7 +67,7 @@ func NewTestEthBroadcaster( lggr := logger.TestLogger(t) ge := config.EVM().GasEstimator() - estimator := gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(config.EVM().GasEstimator(), ge.BlockHistory(), lggr), ge.EIP1559DynamicFees()) + estimator := gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(config.EVM().GasEstimator(), ge.BlockHistory(), lggr), ge.EIP1559DynamicFees(), nil) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, keyStore, estimator) txNonceSyncer := txmgr.NewNonceSyncer(txStore, lggr, ethClient, keyStore) ethBroadcaster := txmgr.NewEvmBroadcaster(txStore, txmgr.NewEvmTxmClient(ethClient), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(config.EVM().GasEstimator()), config.EVM().Transactions(), config.Database().Listener(), keyStore, eb, txBuilder, txNonceSyncer, lggr, checkerFactory, nonceAutoSync) @@ -1123,7 +1123,7 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_Errors(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, eventBroadcaster.Close()) }) lggr := logger.TestLogger(t) - estimator := gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), evmcfg.EVM().GasEstimator().BlockHistory(), lggr), evmcfg.EVM().GasEstimator().EIP1559DynamicFees()) + estimator := gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), evmcfg.EVM().GasEstimator().BlockHistory(), lggr), evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), nil) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), evmcfg.EVM().GasEstimator(), ethKeyStore, estimator) eb = txmgr.NewEvmBroadcaster(txStore, txmgr.NewEvmTxmClient(ethClient), txmgr.NewEvmTxmConfig(evmcfg.EVM()), txmgr.NewEvmTxmFeeConfig(evmcfg.EVM().GasEstimator()), evmcfg.EVM().Transactions(), evmcfg.Database().Listener(), ethKeyStore, eventBroadcaster, txBuilder, nil, lggr, &testCheckerFactory{}, false) require.NoError(t, err) @@ -1772,7 +1772,7 @@ func TestEthBroadcaster_SyncNonce(t *testing.T) { sub.On("Events").Return(make(<-chan pg.Event)) sub.On("Close") eventBroadcaster.On("Subscribe", "insert_on_eth_txes", "").Return(sub, nil) - estimator := gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), evmcfg.EVM().GasEstimator().BlockHistory(), lggr), evmcfg.EVM().GasEstimator().EIP1559DynamicFees()) + estimator := gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), evmcfg.EVM().GasEstimator().BlockHistory(), lggr), evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), nil) 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 3de0c1079bf..3818335298e 100644 --- a/core/chains/evm/txmgr/confirmer_test.go +++ b/core/chains/evm/txmgr/confirmer_test.go @@ -122,7 +122,7 @@ func TestEthConfirmer_Lifecycle(t *testing.T) { estimator := gasmocks.NewEvmEstimator(t) lggr := logger.TestLogger(t) ge := config.EVM().GasEstimator() - feeEstimator := gas.NewWrappedEvmEstimator(estimator, ge.EIP1559DynamicFees()) + feeEstimator := gas.NewWrappedEvmEstimator(estimator, ge.EIP1559DynamicFees(), nil) 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) @@ -1645,7 +1645,7 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing estimator := gasmocks.NewEvmEstimator(t) estimator.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, uint32(0), pkgerrors.Wrapf(commonfee.ErrConnectivity, "transaction...")) ge := ccfg.EVM().GasEstimator() - feeEstimator := gas.NewWrappedEvmEstimator(estimator, ge.EIP1559DynamicFees()) + feeEstimator := gas.NewWrappedEvmEstimator(estimator, ge.EIP1559DynamicFees(), nil) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, feeEstimator) addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", &cltest.FixtureChainID).Return(addresses, nil).Maybe() @@ -1690,7 +1690,7 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing estimator.On("BumpDynamicFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.DynamicFee{}, uint32(0), pkgerrors.Wrapf(commonfee.ErrConnectivity, "transaction...")) // Create confirmer with necessary state ge := ccfg.EVM().GasEstimator() - feeEstimator := gas.NewWrappedEvmEstimator(estimator, ge.EIP1559DynamicFees()) + feeEstimator := gas.NewWrappedEvmEstimator(estimator, ge.EIP1559DynamicFees(), nil) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, feeEstimator) addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", &cltest.FixtureChainID).Return(addresses, nil).Maybe() diff --git a/core/internal/cltest/cltest.go b/core/internal/cltest/cltest.go index 63c1f49a664..4fe03004d97 100644 --- a/core/internal/cltest/cltest.go +++ b/core/internal/cltest/cltest.go @@ -213,7 +213,7 @@ func NewEthConfirmer(t testing.TB, txStore txmgr.EvmTxStore, ethClient evmclient t.Helper() lggr := logger.TestLogger(t) ge := config.EVM().GasEstimator() - estimator := gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(ge, ge.BlockHistory(), lggr), ge.EIP1559DynamicFees()) + estimator := gas.NewWrappedEvmEstimator(gas.NewFixedPriceEstimator(ge, ge.BlockHistory(), lggr), ge.EIP1559DynamicFees(), nil) 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) From 06b61baab640063db26caedb496c56f4e776c1d6 Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Thu, 14 Sep 2023 17:51:42 -0400 Subject: [PATCH 13/14] fix chaintype panic --- .../evm/gas/chainoracles/l1_gas_price_oracle.go | 7 +++++++ core/chains/evm/gas/models.go | 14 +++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go index 1ab2b582097..e031ed115be 100644 --- a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go +++ b/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "golang.org/x/exp/slices" "github.com/smartcontractkit/chainlink/v2/core/assets" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" @@ -59,6 +60,12 @@ const ( PollPeriod = 12 * time.Second ) +var supportedChainTypes = []config.ChainType{config.ChainArbitrum, config.ChainOptimismBedrock} + +func IsL1OracleChain(chainType config.ChainType) bool { + return slices.Contains(supportedChainTypes, chainType) +} + func NewL1GasPriceOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType) L1Oracle { var address, callArgs string switch chainType { diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index f5d3ef934cf..b7f4377767f 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -66,20 +66,24 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, ge "priceMin", geCfg.PriceMin(), ) df := geCfg.EIP1559DynamicFees() + + // create l1Oracle only if it is supported for the chain + var l1Oracle chainoracles.L1Oracle + if chainoracles.IsL1OracleChain(cfg.ChainType()) { + l1Oracle = chainoracles.NewL1GasPriceOracle(lggr, ethClient, cfg.ChainType()) + } switch s { case "Arbitrum": - l1Oracle := chainoracles.NewL1GasPriceOracle(lggr, ethClient, cfg.ChainType()) return NewWrappedEvmEstimator(NewArbitrumEstimator(lggr, geCfg, ethClient, ethClient), df, l1Oracle) case "BlockHistory": - l1Oracle := chainoracles.NewL1GasPriceOracle(lggr, ethClient, cfg.ChainType()) return NewWrappedEvmEstimator(NewBlockHistoryEstimator(lggr, ethClient, cfg, geCfg, bh, *ethClient.ConfiguredChainID()), df, l1Oracle) case "FixedPrice": - return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df, nil) + return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df, l1Oracle) case "Optimism2", "L2Suggested": - return NewWrappedEvmEstimator(NewL2SuggestedPriceEstimator(lggr, ethClient), df, nil) + return NewWrappedEvmEstimator(NewL2SuggestedPriceEstimator(lggr, ethClient), df, l1Oracle) default: lggr.Warnf("GasEstimator: unrecognised mode '%s', falling back to FixedPriceEstimator", s) - return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df, nil) + return NewWrappedEvmEstimator(NewFixedPriceEstimator(geCfg, bh, lggr), df, l1Oracle) } } From 42c5a002592825be16f8c4ec2e5216e55d2a0d7f Mon Sep 17 00:00:00 2001 From: Matt Yang Date: Fri, 15 Sep 2023 18:45:37 -0400 Subject: [PATCH 14/14] address comments --- .../chains/evm/gas/mocks/evm_fee_estimator.go | 12 +++--- core/chains/evm/gas/models.go | 16 ++++---- core/chains/evm/gas/models_test.go | 12 +++--- .../l1_gas_price_oracle.go | 6 +-- .../l1_gas_price_oracle_test.go | 15 +++---- .../mocks/eth_client.go | 3 +- .../mocks/l1_oracle.go | 40 +++++++++---------- .../gas/{chainoracles => rollups}/models.go | 4 +- 8 files changed, 54 insertions(+), 54 deletions(-) rename core/chains/evm/gas/{chainoracles => rollups}/l1_gas_price_oracle.go (96%) rename core/chains/evm/gas/{chainoracles => rollups}/l1_gas_price_oracle_test.go (84%) rename core/chains/evm/gas/{chainoracles => rollups}/mocks/eth_client.go (99%) rename core/chains/evm/gas/{chainoracles => rollups}/mocks/l1_oracle.go (94%) rename core/chains/evm/gas/{chainoracles => rollups}/models.go (85%) diff --git a/core/chains/evm/gas/mocks/evm_fee_estimator.go b/core/chains/evm/gas/mocks/evm_fee_estimator.go index 8fb0409154f..dbca58dcdd5 100644 --- a/core/chains/evm/gas/mocks/evm_fee_estimator.go +++ b/core/chains/evm/gas/mocks/evm_fee_estimator.go @@ -7,8 +7,6 @@ import ( assets "github.com/smartcontractkit/chainlink/v2/core/assets" - chainoracles "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/chainoracles" - context "context" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" @@ -17,6 +15,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" ) @@ -158,15 +158,15 @@ func (_m *EvmFeeEstimator) HealthReport() map[string]error { } // L1Oracle provides a mock function with given fields: -func (_m *EvmFeeEstimator) L1Oracle() chainoracles.L1Oracle { +func (_m *EvmFeeEstimator) L1Oracle() rollups.L1Oracle { ret := _m.Called() - var r0 chainoracles.L1Oracle - if rf, ok := ret.Get(0).(func() chainoracles.L1Oracle); ok { + 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).(chainoracles.L1Oracle) + r0 = ret.Get(0).(rollups.L1Oracle) } } diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index b7f4377767f..c6f8edbf04b 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -16,7 +16,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/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/chainoracles" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/label" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/config" @@ -34,7 +34,7 @@ type EvmFeeEstimator interface { commontypes.HeadTrackable[*evmtypes.Head, common.Hash] // L1Oracle returns the L1 gas price oracle only if the chain has one, e.g. OP stack L2s and Arbitrum. - L1Oracle() chainoracles.L1Oracle + L1Oracle() rollups.L1Oracle GetFee(ctx context.Context, calldata []byte, feeLimit uint32, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint32, err error) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint32, maxFeePrice *assets.Wei, attempts []EvmPriorAttempt) (bumpedFee EvmFee, chainSpecificFeeLimit uint32, err error) @@ -68,9 +68,9 @@ func NewEstimator(lggr logger.Logger, ethClient evmclient.Client, cfg Config, ge df := geCfg.EIP1559DynamicFees() // create l1Oracle only if it is supported for the chain - var l1Oracle chainoracles.L1Oracle - if chainoracles.IsL1OracleChain(cfg.ChainType()) { - l1Oracle = chainoracles.NewL1GasPriceOracle(lggr, ethClient, cfg.ChainType()) + var l1Oracle rollups.L1Oracle + if rollups.IsRollupWithL1Support(cfg.ChainType()) { + l1Oracle = rollups.NewL1GasPriceOracle(lggr, ethClient, cfg.ChainType()) } switch s { case "Arbitrum": @@ -152,13 +152,13 @@ func (fee EvmFee) ValidDynamic() bool { type WrappedEvmEstimator struct { EvmEstimator EIP1559Enabled bool - l1Oracle chainoracles.L1Oracle + l1Oracle rollups.L1Oracle utils.StartStopOnce } var _ EvmFeeEstimator = (*WrappedEvmEstimator)(nil) -func NewWrappedEvmEstimator(e EvmEstimator, eip1559Enabled bool, l1Oracle chainoracles.L1Oracle) EvmFeeEstimator { +func NewWrappedEvmEstimator(e EvmEstimator, eip1559Enabled bool, l1Oracle rollups.L1Oracle) EvmFeeEstimator { return &WrappedEvmEstimator{ EvmEstimator: e, EIP1559Enabled: eip1559Enabled, @@ -223,7 +223,7 @@ func (e *WrappedEvmEstimator) HealthReport() map[string]error { return report } -func (e *WrappedEvmEstimator) L1Oracle() chainoracles.L1Oracle { +func (e *WrappedEvmEstimator) L1Oracle() rollups.L1Oracle { return e.l1Oracle } diff --git a/core/chains/evm/gas/models_test.go b/core/chains/evm/gas/models_test.go index a4cac2db0cb..c1dd9e44ffc 100644 --- a/core/chains/evm/gas/models_test.go +++ b/core/chains/evm/gas/models_test.go @@ -12,8 +12,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" - oraclesMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/chainoracles/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" + rollupMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" ) func TestWrappedEvmEstimator(t *testing.T) { @@ -49,7 +49,7 @@ func TestWrappedEvmEstimator(t *testing.T) { assert.Nil(t, l1Oracle) // expect l1Oracle - oracle := oraclesMocks.NewL1Oracle(t) + oracle := rollupMocks.NewL1Oracle(t) estimator = gas.NewWrappedEvmEstimator(e, false, oracle) l1Oracle = estimator.L1Oracle() assert.Equal(t, oracle, l1Oracle) @@ -135,7 +135,7 @@ func TestWrappedEvmEstimator(t *testing.T) { t.Run("Name", func(t *testing.T) { evmEstimator := mocks.NewEvmEstimator(t) - oracle := oraclesMocks.NewL1Oracle(t) + oracle := rollupMocks.NewL1Oracle(t) evmEstimator.On("Name").Return(mockEvmEstimatorName, nil).Once() @@ -146,7 +146,7 @@ func TestWrappedEvmEstimator(t *testing.T) { t.Run("Start and stop calls both EVM estimator and L1Oracle", func(t *testing.T) { evmEstimator := mocks.NewEvmEstimator(t) - oracle := oraclesMocks.NewL1Oracle(t) + oracle := rollupMocks.NewL1Oracle(t) evmEstimator.On("Name").Return(mockEvmEstimatorName, nil).Times(4) evmEstimator.On("Start", mock.Anything).Return(nil).Twice() @@ -169,7 +169,7 @@ func TestWrappedEvmEstimator(t *testing.T) { t.Run("Read calls both EVM estimator and L1Oracle", func(t *testing.T) { evmEstimator := mocks.NewEvmEstimator(t) - oracle := oraclesMocks.NewL1Oracle(t) + oracle := rollupMocks.NewL1Oracle(t) evmEstimator.On("Ready").Return(nil).Twice() oracle.On("Ready").Return(nil).Once() @@ -185,7 +185,7 @@ func TestWrappedEvmEstimator(t *testing.T) { t.Run("HealthReport merges report from EVM estimator and L1Oracle", func(t *testing.T) { evmEstimator := mocks.NewEvmEstimator(t) - oracle := oraclesMocks.NewL1Oracle(t) + oracle := rollupMocks.NewL1Oracle(t) evmEstimatorKey := "evm" evmEstimatorError := errors.New("evm error") diff --git a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go b/core/chains/evm/gas/rollups/l1_gas_price_oracle.go similarity index 96% rename from core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go rename to core/chains/evm/gas/rollups/l1_gas_price_oracle.go index e031ed115be..13ec5e29dd8 100644 --- a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go +++ b/core/chains/evm/gas/rollups/l1_gas_price_oracle.go @@ -1,4 +1,4 @@ -package chainoracles +package rollups import ( "context" @@ -62,7 +62,7 @@ const ( var supportedChainTypes = []config.ChainType{config.ChainArbitrum, config.ChainOptimismBedrock} -func IsL1OracleChain(chainType config.ChainType) bool { +func IsRollupWithL1Support(chainType config.ChainType) bool { return slices.Contains(supportedChainTypes, chainType) } @@ -160,7 +160,7 @@ func (o *l1GasPriceOracle) refresh() (t *time.Timer) { return } -func (o *l1GasPriceOracle) L1GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) { +func (o *l1GasPriceOracle) GasPrice(_ context.Context) (l1GasPrice *assets.Wei, err error) { ok := o.IfStarted(func() { o.l1GasPriceMu.RLock() l1GasPrice = o.l1GasPrice diff --git a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go b/core/chains/evm/gas/rollups/l1_gas_price_oracle_test.go similarity index 84% rename from core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go rename to core/chains/evm/gas/rollups/l1_gas_price_oracle_test.go index f3d23de0867..9fd2a66201c 100644 --- a/core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go +++ b/core/chains/evm/gas/rollups/l1_gas_price_oracle_test.go @@ -1,4 +1,4 @@ -package chainoracles +package rollups import ( "fmt" @@ -11,8 +11,9 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" + "github.com/smartcontractkit/chainlink/v2/core/assets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/chainoracles/mocks" "github.com/smartcontractkit/chainlink/v2/core/config" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -32,11 +33,11 @@ func TestL1GasPriceOracle(t *testing.T) { oracle := NewL1GasPriceOracle(logger.TestLogger(t), ethClient, config.ChainOptimismBedrock) - _, err := oracle.L1GasPrice(testutils.Context(t)) + _, err := oracle.GasPrice(testutils.Context(t)) assert.EqualError(t, err, "L1GasPriceOracle is not started; cannot estimate gas") }) - t.Run("Calling L1GasPrice on started Arbitrum L1Oracle returns Arbitrum l1GasPrice", func(t *testing.T) { + t.Run("Calling GasPrice on started Arbitrum L1Oracle returns Arbitrum l1GasPrice", func(t *testing.T) { l1BaseFee := big.NewInt(100) ethClient := mocks.NewETHClient(t) @@ -52,13 +53,13 @@ func TestL1GasPriceOracle(t *testing.T) { require.NoError(t, oracle.Start(testutils.Context(t))) t.Cleanup(func() { assert.NoError(t, oracle.Close()) }) - gasPrice, err := oracle.L1GasPrice(testutils.Context(t)) + gasPrice, err := oracle.GasPrice(testutils.Context(t)) require.NoError(t, err) assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) }) - t.Run("Calling L1GasPrice on started OPStack L1Oracle returns OPStack l1GasPrice", func(t *testing.T) { + t.Run("Calling GasPrice on started OPStack L1Oracle returns OPStack l1GasPrice", func(t *testing.T) { l1BaseFee := big.NewInt(200) ethClient := mocks.NewETHClient(t) @@ -74,7 +75,7 @@ func TestL1GasPriceOracle(t *testing.T) { require.NoError(t, oracle.Start(testutils.Context(t))) t.Cleanup(func() { assert.NoError(t, oracle.Close()) }) - gasPrice, err := oracle.L1GasPrice(testutils.Context(t)) + gasPrice, err := oracle.GasPrice(testutils.Context(t)) require.NoError(t, err) assert.Equal(t, assets.NewWei(l1BaseFee), gasPrice) diff --git a/core/chains/evm/gas/chainoracles/mocks/eth_client.go b/core/chains/evm/gas/rollups/mocks/eth_client.go similarity index 99% rename from core/chains/evm/gas/chainoracles/mocks/eth_client.go rename to core/chains/evm/gas/rollups/mocks/eth_client.go index 86228d4d1fe..5389661bc56 100644 --- a/core/chains/evm/gas/chainoracles/mocks/eth_client.go +++ b/core/chains/evm/gas/rollups/mocks/eth_client.go @@ -3,9 +3,8 @@ package mocks import ( - big "math/big" - context "context" + big "math/big" ethereum "github.com/ethereum/go-ethereum" diff --git a/core/chains/evm/gas/chainoracles/mocks/l1_oracle.go b/core/chains/evm/gas/rollups/mocks/l1_oracle.go similarity index 94% rename from core/chains/evm/gas/chainoracles/mocks/l1_oracle.go rename to core/chains/evm/gas/rollups/mocks/l1_oracle.go index cd896370e81..e148c0e9ac4 100644 --- a/core/chains/evm/gas/chainoracles/mocks/l1_oracle.go +++ b/core/chains/evm/gas/rollups/mocks/l1_oracle.go @@ -3,10 +3,10 @@ package mocks import ( - assets "github.com/smartcontractkit/chainlink/v2/core/assets" - context "context" + assets "github.com/smartcontractkit/chainlink/v2/core/assets" + mock "github.com/stretchr/testify/mock" ) @@ -29,24 +29,8 @@ func (_m *L1Oracle) Close() error { return r0 } -// HealthReport provides a mock function with given fields: -func (_m *L1Oracle) HealthReport() map[string]error { - ret := _m.Called() - - var r0 map[string]error - if rf, ok := ret.Get(0).(func() map[string]error); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]error) - } - } - - return r0 -} - -// L1GasPrice provides a mock function with given fields: ctx -func (_m *L1Oracle) L1GasPrice(ctx context.Context) (*assets.Wei, error) { +// GasPrice provides a mock function with given fields: ctx +func (_m *L1Oracle) GasPrice(ctx context.Context) (*assets.Wei, error) { ret := _m.Called(ctx) var r0 *assets.Wei @@ -71,6 +55,22 @@ func (_m *L1Oracle) L1GasPrice(ctx context.Context) (*assets.Wei, error) { return r0, r1 } +// HealthReport provides a mock function with given fields: +func (_m *L1Oracle) HealthReport() map[string]error { + ret := _m.Called() + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + // Name provides a mock function with given fields: func (_m *L1Oracle) Name() string { ret := _m.Called() diff --git a/core/chains/evm/gas/chainoracles/models.go b/core/chains/evm/gas/rollups/models.go similarity index 85% rename from core/chains/evm/gas/chainoracles/models.go rename to core/chains/evm/gas/rollups/models.go index e95fb036aaf..83ae29f4ea9 100644 --- a/core/chains/evm/gas/chainoracles/models.go +++ b/core/chains/evm/gas/rollups/models.go @@ -1,4 +1,4 @@ -package chainoracles +package rollups import ( "context" @@ -14,5 +14,5 @@ import ( type L1Oracle interface { services.ServiceCtx - L1GasPrice(ctx context.Context) (*assets.Wei, error) + GasPrice(ctx context.Context) (*assets.Wei, error) }