-
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.
- Loading branch information
1 parent
f0c928b
commit f13e0d7
Showing
8 changed files
with
251 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 #wip |
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
164 changes: 164 additions & 0 deletions
164
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,164 @@ | ||
package rollups | ||
|
||
import ( | ||
"context" | ||
"encoding/hex" | ||
"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/oracletype" | ||
) | ||
|
||
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, daOracleConfig evmconfig.DAOracle) (*customCalldataDAOracle, error) { | ||
if daOracleConfig.OracleType() != oracletype.CustomCalldata { | ||
return nil, fmt.Errorf("expected CustomCalldata oracle type, got %s", daOracleConfig.OracleType()) | ||
} | ||
return &customCalldataDAOracle{ | ||
client: ethClient, | ||
pollPeriod: PollPeriod, | ||
logger: logger.Sugared(logger.Named(lggr, fmt.Sprintf("CustomCalldataDAOracle(%s)", daOracleConfig.OracleType()))), | ||
|
||
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, fmt.Errorf("DAGasOracle is not started; cannot estimate gas") | ||
} | ||
if daGasPrice == nil { | ||
return daGasPrice, fmt.Errorf("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, fmt.Errorf("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 | ||
} |
69 changes: 69 additions & 0 deletions
69
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,69 @@ | ||
package rollups | ||
|
||
import ( | ||
"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/oracletype" | ||
"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("panics if oracle type is not CustomCalldata", func(t *testing.T) { | ||
ethClient := mocks.NewL1OracleClient(t) | ||
daOracleConfig := CreateTestDAOracle(t, oracletype.OPStack, oracleAddress, "") | ||
_, err := NewCustomCalldataDAOracle(logger.Test(t), ethClient, daOracleConfig) | ||
require.Error(t, err) | ||
}) | ||
} | ||
|
||
func TestCustomCalldataDAOracle_getCustomCalldataGasPrice(t *testing.T) { | ||
oracleAddress := utils.RandomAddress().String() | ||
t.Parallel() | ||
|
||
t.Run("correctly fetches gas price if chain has custom calldata", func(t *testing.T) { | ||
ethClient := mocks.NewL1OracleClient(t) | ||
expectedPriceHex := "0x0000000000000000000000000000000000000000000000000000000000000032" | ||
|
||
daOracleConfig := CreateTestDAOracle(t, oracletype.CustomCalldata, oracleAddress, "0x0000000000000000000000000000000000001234") | ||
oracle, err := NewCustomCalldataDAOracle(logger.Test(t), ethClient, 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, oracletype.CustomCalldata, oracleAddress, "0xblahblahblah") | ||
oracle, err := NewCustomCalldataDAOracle(logger.Test(t), ethClient, daOracleConfig) | ||
require.NoError(t, err) | ||
|
||
_, 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