Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EvmFeeEstimator return Optimistic Rollup's L1BaseFee #10557

Merged
merged 15 commits into from
Sep 16, 2023
168 changes: 168 additions & 0 deletions core/chains/evm/gas/chainoracles/l1_gas_price_oracle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package chainoracles
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Just wondering, if this name is appropriate.
This seems to be a package for Rollups.
Could rollups be a better name?


import (
"context"
"fmt"
"math/big"
"sync"
"time"

"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"
"github.com/smartcontractkit/chainlink/v2/core/config"
"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/smartcontractkit/chainlink/v2/core/utils"
)

//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 ethClient
pollPeriod time.Duration
logger logger.Logger
address string
selector string

l1GasPriceMu sync.RWMutex
l1GasPrice *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 on OP stack chain.
// This is the case for Optimism and Base.
OPGasOracleAddress = "0x420000000000000000000000000000000000000F"
// GasOracle_l1BaseFee is the a hex encoded call to:
// `function l1BaseFee() external view returns (uint256);`
OPGasOracle_l1BaseFee = "519b4bd3"

// Interval at which to poll for L1BaseFee. A good starting point is the L1 block time.
PollPeriod = 12 * time.Second
)

func NewL1GasPriceOracle(lggr logger.Logger, ethClient ethClient, chainType config.ChainType) L1Oracle {
var address, selector string
switch chainType {
case config.ChainArbitrum:
address = ArbGasInfoAddress
selector = ArbGasInfo_getL1BaseFeeEstimate
case config.ChainOptimismBedrock:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Base also uses OptimismBedorck chaintype, can we confirm the address and the hex code are the same?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

address = OPGasOracleAddress
selector = OPGasOracle_l1BaseFee
default:
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are calling this NewL1GasPriceOracle() only for chains that support it, the default case here should just call panic().
This will help surface up a misconfig or error faster.

}

return &l1GasPriceOracle{
client: ethClient,
pollPeriod: PollPeriod,
logger: lggr.Named(fmt.Sprintf("L1GasPriceOracle(%s)", chainType)),
address: address,
selector: selector,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about call instead, since this is a hex-encoded call? Selector doesn't provide much context.

chInitialised: make(chan struct{}),
chStop: make(chan struct{}),
chDone: make(chan struct{}),
}
}

func (o *l1GasPriceOracle) Name() string {
return o.logger.Name()
}

func (o *l1GasPriceOracle) Start(ctx context.Context) error {
return o.StartOnce(o.Name(), func() error {
go o.run()
<-o.chInitialised
return nil
})
}
func (o *l1GasPriceOracle) Close() error {
return o.StopOnce(o.Name(), func() error {
close(o.chStop)
<-o.chDone
return nil
})
}

func (o *l1GasPriceOracle) Ready() error { return o.StartStopOnce.Ready() }

func (o *l1GasPriceOracle) HealthReport() map[string]error {
return map[string]error{o.Name(): o.StartStopOnce.Healthy()}
}

func (o *l1GasPriceOracle) run() {
defer close(o.chDone)

t := o.refresh()
close(o.chInitialised)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of from here, close this from within refresh(), once we successfully are able to set l1GasPrice there.
This way, this component is in started mode only after it fetches a valid l1GasPrice.

Copy link
Contributor Author

@matYang matYang Sep 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Start() call is blocked on the chInitialised channel, so we need to close it regardless of fetching prices (refresh()) was successful or not right, closing it once here makes sense to me. This pattern is same in L2Suggested and Arbitrum estimators.


for {
select {
case <-o.chStop:
return
case <-t.C:
t = o.refresh()
}
}
}

func (o *l1GasPriceOracle) refresh() (t *time.Timer) {
t = time.NewTimer(utils.WithJitter(o.pollPeriod))

ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout())
defer cancel()

precompile := common.HexToAddress(o.address)
b, err := o.client.CallContract(ctx, ethereum.CallMsg{
To: &precompile,
Data: common.Hex2Bytes(o.selector),
}, big.NewInt(-1))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be nil instead. Not sure why on Arbitrum Estimator is also -1

if err != nil {
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an error log here too

}

if len(b) != 32 { // returns uint256;
o.logger.Warnf("return data length (%d) different than expected (%d)", len(b), 32)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a critical error. It indicates the response format has changed.

return
}
price := new(big.Int).SetBytes(b)

o.l1GasPriceMu.Lock()
defer o.l1GasPriceMu.Unlock()
o.l1GasPrice = assets.NewWei(price)
return
}

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
}
83 changes: 83 additions & 0 deletions core/chains/evm/gas/chainoracles/l1_gas_price_oracle_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
59 changes: 59 additions & 0 deletions core/chains/evm/gas/chainoracles/mocks/eth_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading