-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CCIP-3710 create new custom calldata L1 (DA) gas oracle (#14710)
* Add new custom calldata DA oracle * added more tests * rename logger * lint
- Loading branch information
1 parent
951c392
commit 3c40e9b
Showing
9 changed files
with
315 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"chainlink": patch | ||
--- | ||
|
||
Adds new custom calldata DA oracle #added |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
169 changes: 169 additions & 0 deletions
169
core/chains/evm/gas/rollups/custom_calldata_da_oracle.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
package rollups | ||
|
||
import ( | ||
"context" | ||
"encoding/hex" | ||
"errors" | ||
"fmt" | ||
"math/big" | ||
"strings" | ||
"sync" | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum" | ||
|
||
"github.com/smartcontractkit/chainlink-common/pkg/logger" | ||
"github.com/smartcontractkit/chainlink-common/pkg/services" | ||
|
||
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" | ||
evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" | ||
evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" | ||
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" | ||
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" | ||
) | ||
|
||
type customCalldataDAOracle struct { | ||
services.StateMachine | ||
client l1OracleClient | ||
pollPeriod time.Duration | ||
logger logger.SugaredLogger | ||
|
||
daOracleConfig evmconfig.DAOracle | ||
daGasPriceMu sync.RWMutex | ||
daGasPrice priceEntry | ||
|
||
chInitialized chan struct{} | ||
chStop services.StopChan | ||
chDone chan struct{} | ||
} | ||
|
||
// NewCustomCalldataDAOracle creates a new custom calldata DA oracle. The CustomCalldataDAOracle fetches gas price from | ||
// whatever function is specified in the DAOracle's CustomGasPriceCalldata field. This allows for more flexibility when | ||
// chains have custom DA gas calculation methods. | ||
func NewCustomCalldataDAOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chaintype.ChainType, daOracleConfig evmconfig.DAOracle) (*customCalldataDAOracle, error) { | ||
if daOracleConfig.OracleType() != toml.DAOracleCustomCalldata { | ||
return nil, fmt.Errorf("expected %s oracle type, got %s", toml.DAOracleCustomCalldata, daOracleConfig.OracleType()) | ||
} | ||
if daOracleConfig.CustomGasPriceCalldata() == "" { | ||
return nil, errors.New("CustomGasPriceCalldata is required") | ||
} | ||
return &customCalldataDAOracle{ | ||
client: ethClient, | ||
pollPeriod: PollPeriod, | ||
logger: logger.Sugared(logger.Named(lggr, fmt.Sprintf("CustomCalldataDAOracle(%s)", chainType))), | ||
|
||
daOracleConfig: daOracleConfig, | ||
|
||
chInitialized: make(chan struct{}), | ||
chStop: make(chan struct{}), | ||
chDone: make(chan struct{}), | ||
}, nil | ||
} | ||
|
||
func (o *customCalldataDAOracle) Name() string { | ||
return o.logger.Name() | ||
} | ||
|
||
func (o *customCalldataDAOracle) Start(_ context.Context) error { | ||
return o.StartOnce(o.Name(), func() error { | ||
go o.run() | ||
<-o.chInitialized | ||
return nil | ||
}) | ||
} | ||
|
||
func (o *customCalldataDAOracle) Close() error { | ||
return o.StopOnce(o.Name(), func() error { | ||
close(o.chStop) | ||
<-o.chDone | ||
return nil | ||
}) | ||
} | ||
|
||
func (o *customCalldataDAOracle) HealthReport() map[string]error { | ||
return map[string]error{o.Name(): o.Healthy()} | ||
} | ||
|
||
func (o *customCalldataDAOracle) run() { | ||
defer close(o.chDone) | ||
|
||
o.refresh() | ||
close(o.chInitialized) | ||
|
||
t := services.TickerConfig{ | ||
Initial: o.pollPeriod, | ||
JitterPct: services.DefaultJitter, | ||
}.NewTicker(o.pollPeriod) | ||
defer t.Stop() | ||
|
||
for { | ||
select { | ||
case <-o.chStop: | ||
return | ||
case <-t.C: | ||
o.refresh() | ||
} | ||
} | ||
} | ||
|
||
func (o *customCalldataDAOracle) refresh() { | ||
err := o.refreshWithError() | ||
if err != nil { | ||
o.logger.Criticalw("Failed to refresh gas price", "err", err) | ||
o.SvcErrBuffer.Append(err) | ||
} | ||
} | ||
|
||
func (o *customCalldataDAOracle) refreshWithError() error { | ||
ctx, cancel := o.chStop.CtxCancel(evmclient.ContextWithDefaultTimeout()) | ||
defer cancel() | ||
|
||
price, err := o.getCustomCalldataGasPrice(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
o.daGasPriceMu.Lock() | ||
defer o.daGasPriceMu.Unlock() | ||
o.daGasPrice = priceEntry{price: assets.NewWei(price), timestamp: time.Now()} | ||
return nil | ||
} | ||
|
||
func (o *customCalldataDAOracle) GasPrice(_ context.Context) (daGasPrice *assets.Wei, err error) { | ||
var timestamp time.Time | ||
ok := o.IfStarted(func() { | ||
o.daGasPriceMu.RLock() | ||
daGasPrice = o.daGasPrice.price | ||
timestamp = o.daGasPrice.timestamp | ||
o.daGasPriceMu.RUnlock() | ||
}) | ||
if !ok { | ||
return daGasPrice, errors.New("DAGasOracle is not started; cannot estimate gas") | ||
} | ||
if daGasPrice == nil { | ||
return daGasPrice, errors.New("failed to get DA gas price; gas price not set") | ||
} | ||
// Validate the price has been updated within the pollPeriod * 2 | ||
// Allowing double the poll period before declaring the price stale to give ample time for the refresh to process | ||
if time.Since(timestamp) > o.pollPeriod*2 { | ||
return daGasPrice, errors.New("gas price is stale") | ||
} | ||
return | ||
} | ||
|
||
func (o *customCalldataDAOracle) getCustomCalldataGasPrice(ctx context.Context) (*big.Int, error) { | ||
daOracleAddress := o.daOracleConfig.OracleAddress().Address() | ||
calldata := strings.TrimPrefix(o.daOracleConfig.CustomGasPriceCalldata(), "0x") | ||
calldataBytes, err := hex.DecodeString(calldata) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to decode custom fee method calldata: %w", err) | ||
} | ||
b, err := o.client.CallContract(ctx, ethereum.CallMsg{ | ||
To: &daOracleAddress, | ||
Data: calldataBytes, | ||
}, nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("custom fee method call failed: %w", err) | ||
} | ||
return new(big.Int).SetBytes(b), nil | ||
} |
102 changes: 102 additions & 0 deletions
102
core/chains/evm/gas/rollups/custom_calldata_da_oracle_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package rollups | ||
|
||
import ( | ||
"errors" | ||
"math/big" | ||
"testing" | ||
|
||
"github.com/ethereum/go-ethereum" | ||
"github.com/ethereum/go-ethereum/common/hexutil" | ||
|
||
"github.com/smartcontractkit/chainlink-common/pkg/utils/tests" | ||
|
||
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" | ||
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" | ||
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" | ||
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" | ||
|
||
"github.com/stretchr/testify/mock" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/smartcontractkit/chainlink-common/pkg/logger" | ||
) | ||
|
||
func TestCustomCalldataDAOracle_NewCustomCalldata(t *testing.T) { | ||
oracleAddress := utils.RandomAddress().String() | ||
t.Parallel() | ||
|
||
t.Run("throws error if oracle type is not custom_calldata", func(t *testing.T) { | ||
ethClient := mocks.NewL1OracleClient(t) | ||
daOracleConfig := CreateTestDAOracle(t, toml.DAOracleOPStack, oracleAddress, "") | ||
_, err := NewCustomCalldataDAOracle(logger.Test(t), ethClient, chaintype.ChainArbitrum, daOracleConfig) | ||
require.Error(t, err) | ||
}) | ||
|
||
t.Run("throws error if CustomGasPriceCalldata is empty", func(t *testing.T) { | ||
ethClient := mocks.NewL1OracleClient(t) | ||
|
||
daOracleConfig := CreateTestDAOracle(t, toml.DAOracleCustomCalldata, oracleAddress, "") | ||
_, err := NewCustomCalldataDAOracle(logger.Test(t), ethClient, chaintype.ChainCelo, daOracleConfig) | ||
require.Error(t, err) | ||
}) | ||
|
||
t.Run("correctly creates custom calldata DA oracle", func(t *testing.T) { | ||
ethClient := mocks.NewL1OracleClient(t) | ||
calldata := "0x0000000000000000000000000000000000001234" | ||
|
||
daOracleConfig := CreateTestDAOracle(t, toml.DAOracleCustomCalldata, oracleAddress, calldata) | ||
oracle, err := NewCustomCalldataDAOracle(logger.Test(t), ethClient, chaintype.ChainZkSync, daOracleConfig) | ||
require.NoError(t, err) | ||
require.NotNil(t, oracle) | ||
}) | ||
} | ||
|
||
func TestCustomCalldataDAOracle_getCustomCalldataGasPrice(t *testing.T) { | ||
oracleAddress := utils.RandomAddress().String() | ||
t.Parallel() | ||
|
||
t.Run("correctly fetches gas price if DA oracle config has custom calldata", func(t *testing.T) { | ||
ethClient := mocks.NewL1OracleClient(t) | ||
expectedPriceHex := "0x32" // 50 | ||
|
||
daOracleConfig := CreateTestDAOracle(t, toml.DAOracleCustomCalldata, oracleAddress, "0x0000000000000000000000000000000000001234") | ||
oracle, err := NewCustomCalldataDAOracle(logger.Test(t), ethClient, chaintype.ChainZkSync, daOracleConfig) | ||
require.NoError(t, err) | ||
|
||
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) | ||
require.NotNil(t, callMsg.To) | ||
require.Equal(t, oracleAddress, callMsg.To.String()) | ||
require.Nil(t, blockNumber) | ||
}).Return(hexutil.MustDecode(expectedPriceHex), nil).Once() | ||
|
||
price, err := oracle.getCustomCalldataGasPrice(tests.Context(t)) | ||
require.NoError(t, err) | ||
require.Equal(t, big.NewInt(50), price) | ||
}) | ||
|
||
t.Run("throws error if custom calldata fails to decode", func(t *testing.T) { | ||
ethClient := mocks.NewL1OracleClient(t) | ||
|
||
daOracleConfig := CreateTestDAOracle(t, toml.DAOracleCustomCalldata, oracleAddress, "0xblahblahblah") | ||
oracle, err := NewCustomCalldataDAOracle(logger.Test(t), ethClient, chaintype.ChainCelo, daOracleConfig) | ||
require.NoError(t, err) | ||
|
||
_, err = oracle.getCustomCalldataGasPrice(tests.Context(t)) | ||
require.Error(t, err) | ||
}) | ||
|
||
t.Run("throws error if CallContract call fails", func(t *testing.T) { | ||
ethClient := mocks.NewL1OracleClient(t) | ||
|
||
daOracleConfig := CreateTestDAOracle(t, toml.DAOracleCustomCalldata, oracleAddress, "0x0000000000000000000000000000000000000000000000000000000000000032") | ||
oracle, err := NewCustomCalldataDAOracle(logger.Test(t), ethClient, chaintype.ChainCelo, daOracleConfig) | ||
require.NoError(t, err) | ||
|
||
ethClient.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("RPC failure")).Once() | ||
|
||
_, err = oracle.getCustomCalldataGasPrice(tests.Context(t)) | ||
require.Error(t, err) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters