diff --git a/.changeset/curly-birds-guess.md b/.changeset/curly-birds-guess.md new file mode 100644 index 0000000000..c66bd54178 --- /dev/null +++ b/.changeset/curly-birds-guess.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Fixed deadlock in RPCClient causing CL Node to stop performing RPC requests for the affected chain #bugfix diff --git a/.changeset/curly-onions-tell.md b/.changeset/curly-onions-tell.md new file mode 100644 index 0000000000..249f616c01 --- /dev/null +++ b/.changeset/curly-onions-tell.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#changed Make Mantle use default OP stack l1 gas oracle in core diff --git a/.changeset/eight-bees-speak.md b/.changeset/eight-bees-speak.md new file mode 100644 index 0000000000..9c8ebe428d --- /dev/null +++ b/.changeset/eight-bees-speak.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#bugfix head reporter non-zero reporting period diff --git a/.changeset/four-kangaroos-appear.md b/.changeset/four-kangaroos-appear.md new file mode 100644 index 0000000000..b8ef32ff69 --- /dev/null +++ b/.changeset/four-kangaroos-appear.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Add config validation so it requires ws url when http polling disabled #bugfix diff --git a/.changeset/great-timers-agree.md b/.changeset/great-timers-agree.md new file mode 100644 index 0000000000..bfa27761fb --- /dev/null +++ b/.changeset/great-timers-agree.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Updated gas limit estimation feature to set From address #internal diff --git a/.changeset/green-eagles-deliver.md b/.changeset/green-eagles-deliver.md new file mode 100644 index 0000000000..179b93b108 --- /dev/null +++ b/.changeset/green-eagles-deliver.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Handle zkEVM node level OOC error as TerminallyStuck #internal diff --git a/.changeset/happy-feet-rhyme.md b/.changeset/happy-feet-rhyme.md new file mode 100644 index 0000000000..6e1697d96a --- /dev/null +++ b/.changeset/happy-feet-rhyme.md @@ -0,0 +1,11 @@ +--- +"chainlink": minor +--- + +This PR introduce few changes: +- Add a new config option `EVM.NodePool.NewHeadsPollInterval` (0 by default indicate disabled), which is an interval for polling new block periodically using http client rather than subscribe to ws feed. +- Updated new head handler for polling new head over http, and register the subscription in node lifecycle logic. +- If the polling new heads is enabled, WS new heads subscription will be replaced with the new http based polling. + +Note: There will be another PR for making WS URL optional with some extra condition. +#added diff --git a/.changeset/hot-laws-deny.md b/.changeset/hot-laws-deny.md new file mode 100644 index 0000000000..d71783d1b7 --- /dev/null +++ b/.changeset/hot-laws-deny.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal log info on missed finalized head instead of returning an error diff --git a/.changeset/khaki-tigers-agree.md b/.changeset/khaki-tigers-agree.md new file mode 100644 index 0000000000..ba9e56cc1d --- /dev/null +++ b/.changeset/khaki-tigers-agree.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#added #nops Add Zircuit Configs diff --git a/.changeset/kind-numbers-melt.md b/.changeset/kind-numbers-melt.md new file mode 100644 index 0000000000..43e647399c --- /dev/null +++ b/.changeset/kind-numbers-melt.md @@ -0,0 +1,13 @@ +--- +"chainlink": minor +--- + +Adding feature flag for `LogBroadcaster` called `LogBroadcasterEnabled`, which is `true` by default to support backwards compatibility. +Adding `LogBroadcasterEnabled` allows certain chains to completely disable the `LogBroadcaster` feature, which is an old feature (getting replaced by logPoller) that only few products are using it: +* OCR1 Median +* *OCR2 Median when ChainReader is disabled +* *pre-OCR2 Keeper +* Flux Monitor +* Direct RequestOCR1 Median + +#added diff --git a/.changeset/loud-windows-call.md b/.changeset/loud-windows-call.md new file mode 100644 index 0000000000..6dc8d6fac7 --- /dev/null +++ b/.changeset/loud-windows-call.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Added gas limit estimation feature to EVM gas estimators. Introduced a new config `EVM.GasEstimator.EstimateLimit` to toggle this feature. #added diff --git a/.changeset/moody-rules-agree.md b/.changeset/moody-rules-agree.md new file mode 100644 index 0000000000..ef1f3bcaf6 --- /dev/null +++ b/.changeset/moody-rules-agree.md @@ -0,0 +1,8 @@ +--- +"chainlink": patch +--- + +- register polling subscription to avoid subscription leaking when rpc client gets closed. +- add a temporary special treatment for SubscribeNewHead before we replace it with SubscribeToHeads. Add a goroutine that forwards new head from poller to caller channel. +- fix a deadlock in poller, by using a new lock for subs slice in rpc client. +#bugfix diff --git a/.changeset/pretty-worms-smell.md b/.changeset/pretty-worms-smell.md new file mode 100644 index 0000000000..9633de0950 --- /dev/null +++ b/.changeset/pretty-worms-smell.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Added a custom client error message for Mantle to capture NonceTooLow error. #added diff --git a/.changeset/proud-jokes-exercise.md b/.changeset/proud-jokes-exercise.md new file mode 100644 index 0000000000..4e36d139de --- /dev/null +++ b/.changeset/proud-jokes-exercise.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#added Report new heads as a telemetry to OTI diff --git a/.changeset/seven-kiwis-run.md b/.changeset/seven-kiwis-run.md new file mode 100644 index 0000000000..3b56117c46 --- /dev/null +++ b/.changeset/seven-kiwis-run.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Added custom client error messages for Mantle to capture InsufficientEth and Fatal errors. #added diff --git a/.changeset/shiny-hornets-pretend.md b/.changeset/shiny-hornets-pretend.md new file mode 100644 index 0000000000..ff9946f4e7 --- /dev/null +++ b/.changeset/shiny-hornets-pretend.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Introduce new gas estimator #internal diff --git a/.changeset/silly-lies-boil.md b/.changeset/silly-lies-boil.md new file mode 100644 index 0000000000..b2a5084a36 --- /dev/null +++ b/.changeset/silly-lies-boil.md @@ -0,0 +1,8 @@ +--- +"chainlink": minor +--- + +Make websocket URL `WSURL` for `EVM.Nodes` optional, and apply logic so that: +* If WS URL was not provided, SubscribeFilterLogs should fail with an explicit error +* If WS URL was not provided LogBroadcaster should be disabled +#nops diff --git a/.changeset/swift-pumas-taste.md b/.changeset/swift-pumas-taste.md new file mode 100644 index 0000000000..eb08662e20 --- /dev/null +++ b/.changeset/swift-pumas-taste.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal add head report chain_id diff --git a/.changeset/tender-lemons-obey.md b/.changeset/tender-lemons-obey.md new file mode 100644 index 0000000000..2d6cb774b0 --- /dev/null +++ b/.changeset/tender-lemons-obey.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Enable FeeHistory estimator for Polygon zkEVM #nops diff --git a/.changeset/two-mugs-complain.md b/.changeset/two-mugs-complain.md new file mode 100644 index 0000000000..77cdcbfe9e --- /dev/null +++ b/.changeset/two-mugs-complain.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Edited the Optimism Stack L1 Oracle to add support for Mantle #added diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a4d82ab67f..54cd2491b4 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -170,6 +170,7 @@ jobs: only-new-issues: false # disabled for PRs due to unreliability args: --out-format colored-line-number,checkstyle:golangci-lint-report.xml working-directory: ${{ matrix.project.path }} + continue-on-error: true build-chainlink: environment: integration @@ -584,7 +585,7 @@ jobs: test_config_chainlink_version: ${{ inputs.evm-ref || github.sha }} test_config_selected_networks: ${{ env.SELECTED_NETWORKS }} test_config_logging_run_id: ${{ github.run_id }} - test_config_logstream_log_targets: ${{ vars.LOGSTREAM_LOG_TARGETS }} + test_config_logstream_log_targets: "file" test_config_test_log_collect: ${{ vars.TEST_LOG_COLLECT }} cl_repo: ${{ env.CHAINLINK_IMAGE }} cl_image_tag: ${{ inputs.evm-ref || github.sha }}${{ matrix.product.tag_suffix }} @@ -606,7 +607,7 @@ jobs: go_coverage_src_dir: /var/tmp/go-coverage go_coverage_dest_dir: ${{ github.workspace }}/.covdata DEFAULT_CHAINLINK_IMAGE: ${{ env.CHAINLINK_IMAGE }} - DEFAULT_LOKI_TENANT_ID: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} + DEFAULT_LOKI_TENANT_ID: "promtail" DEFAULT_LOKI_ENDPOINT: https://${{ secrets.GRAFANA_INTERNAL_HOST }}/loki/api/v1/push DEFAULT_LOKI_BASIC_AUTH: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} DEFAULT_GRAFANA_BASE_URL: "http://localhost:8080/primary" diff --git a/.mockery.yaml b/.mockery.yaml index 7fb335620c..07d4fbed65 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -65,6 +65,9 @@ packages: feeEstimatorClient: config: mockname: FeeEstimatorClient + feeHistoryEstimatorClient: + config: + mockname: FeeHistoryEstimatorClient EvmEstimator: github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups: interfaces: @@ -258,11 +261,20 @@ packages: ORM: Runner: PipelineParamUnmarshaler: - github.com/smartcontractkit/chainlink/v2/core/services/promreporter: + github.com/smartcontractkit/chainlink/v2/core/services/headreporter: config: - dir: core/internal/mocks + dir: "{{ .InterfaceDir }}" + filename: "{{ .InterfaceName | snakecase }}_mock.go" + inpackage: true + mockname: "Mock{{ .InterfaceName | camelcase }}" interfaces: + HeadReporter: PrometheusBackend: + github.com/smartcontractkit/libocr/commontypes: + config: + dir: "common/types/mocks" + interfaces: + MonitoringEndpoint: github.com/smartcontractkit/chainlink/v2/core/services/relay/evm: interfaces: BatchCaller: @@ -297,6 +309,15 @@ packages: interfaces: Config: FeeConfig: + github.com/smartcontractkit/chainlink/v2/core/services/telemetry: + config: + dir: "{{ .InterfaceDir }}" + filename: "{{ .InterfaceName | snakecase }}_mock.go" + inpackage: true + mockname: "Mock{{ .InterfaceName | camelcase }}" + interfaces: + MonitoringEndpointGenerator: + IngressAgent: github.com/smartcontractkit/chainlink/v2/core/services/webhook: interfaces: ExternalInitiatorManager: @@ -450,7 +471,7 @@ packages: filename: optimism_portal2_interface.go outpkg: mock_optimism_portal_2 interfaces: - OptimismPortal2Interface: + OptimismPortal2Interface: github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_dispute_game_factory: config: dir: core/gethwrappers/liquiditymanager/mocks/mock_optimism_dispute_game_factory/ @@ -481,12 +502,20 @@ packages: PriceRegistryReader: config: filename: price_registry_reader_mock.go + FeeEstimatorConfigReader: + config: + filename: fee_estimator_config_mock.go TokenPoolReader: config: filename: token_pool_reader_mock.go USDCReader: config: filename: usdc_reader_mock.go + github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig: + interfaces: + GasPriceInterceptor: + config: + filename: gas_price_interceptor_mock.go github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/batchreader: config: filename: token_pool_batched_reader_mock.go diff --git a/common/client/node.go b/common/client/node.go index d6543c772a..7885fe7676 100644 --- a/common/client/node.go +++ b/common/client/node.go @@ -45,6 +45,7 @@ type NodeConfig interface { FinalizedBlockPollInterval() time.Duration EnforceRepeatableRead() bool DeathDeclarationDelay() time.Duration + NewHeadsPollInterval() time.Duration } type ChainConfig interface { @@ -90,14 +91,14 @@ type node[ services.StateMachine lfcLog logger.Logger name string - id int32 + id int chainID CHAIN_ID nodePoolCfg NodeConfig chainCfg ChainConfig order int32 chainFamily string - ws url.URL + ws *url.URL http *url.URL rpc RPC @@ -120,10 +121,10 @@ func NewNode[ nodeCfg NodeConfig, chainCfg ChainConfig, lggr logger.Logger, - wsuri url.URL, + wsuri *url.URL, httpuri *url.URL, name string, - id int32, + id int, chainID CHAIN_ID, nodeOrder int32, rpc RPC, @@ -135,8 +136,10 @@ func NewNode[ n.chainID = chainID n.nodePoolCfg = nodeCfg n.chainCfg = chainCfg - n.ws = wsuri n.order = nodeOrder + if wsuri != nil { + n.ws = wsuri + } if httpuri != nil { n.http = httpuri } @@ -156,7 +159,10 @@ func NewNode[ } func (n *node[CHAIN_ID, HEAD, RPC]) String() string { - s := fmt.Sprintf("(%s)%s:%s", Primary.String(), n.name, n.ws.String()) + s := fmt.Sprintf("(%s)%s", Primary.String(), n.name) + if n.ws != nil { + s = s + fmt.Sprintf(":%s", n.ws.String()) + } if n.http != nil { s = s + fmt.Sprintf(":%s", n.http.String()) } diff --git a/common/client/node_test.go b/common/client/node_test.go index 3b971e8490..539964691c 100644 --- a/common/client/node_test.go +++ b/common/client/node_test.go @@ -20,6 +20,11 @@ type testNodeConfig struct { enforceRepeatableRead bool finalizedBlockPollInterval time.Duration deathDeclarationDelay time.Duration + newHeadsPollInterval time.Duration +} + +func (n testNodeConfig) NewHeadsPollInterval() time.Duration { + return n.newHeadsPollInterval } func (n testNodeConfig) PollFailureThreshold() uint32 { @@ -62,10 +67,10 @@ type testNodeOpts struct { config testNodeConfig chainConfig clientMocks.ChainConfig lggr logger.Logger - wsuri url.URL + wsuri *url.URL httpuri *url.URL name string - id int32 + id int chainID types.ID nodeOrder int32 rpc *mockNodeClient[types.ID, Head] diff --git a/common/fee/models.go b/common/fee/models.go index 0568a2f143..0cc479d356 100644 --- a/common/fee/models.go +++ b/common/fee/models.go @@ -14,6 +14,7 @@ var ( ErrBumpFeeExceedsLimit = errors.New("fee bump exceeds limit") ErrBump = errors.New("fee bump failed") ErrConnectivity = errors.New("transaction propagation issue: transactions are not being mined") + ErrFeeLimitTooLow = errors.New("provided fee limit too low") ) func IsBumpErr(err error) bool { @@ -63,7 +64,7 @@ func CalculateBumpedFee( // Returns highest bumped fee price of originalFeePrice bumped by fixed units or percentage. func MaxBumpedFee(originalFeePrice *big.Int, feeBumpPercent uint16, feeBumpUnits *big.Int) *big.Int { return bigmath.Max( - addPercentage(originalFeePrice, feeBumpPercent), + AddPercentage(originalFeePrice, feeBumpPercent), new(big.Int).Add(originalFeePrice, feeBumpUnits), ) } diff --git a/common/fee/utils.go b/common/fee/utils.go index 26323e11e2..3d4b001e83 100644 --- a/common/fee/utils.go +++ b/common/fee/utils.go @@ -18,7 +18,7 @@ func ApplyMultiplier(feeLimit uint64, multiplier float32) (uint64, error) { } // Returns the input value increased by the given percentage. -func addPercentage(value *big.Int, percentage uint16) *big.Int { +func AddPercentage(value *big.Int, percentage uint16) *big.Int { bumped := new(big.Int) bumped.Mul(value, big.NewInt(int64(100+percentage))) bumped.Div(bumped, big.NewInt(100)) diff --git a/common/txmgr/broadcaster.go b/common/txmgr/broadcaster.go index be3d3ca2f6..1606f58ce0 100644 --- a/common/txmgr/broadcaster.go +++ b/common/txmgr/broadcaster.go @@ -20,6 +20,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink/v2/common/client" + commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" "github.com/smartcontractkit/chainlink/v2/common/types" @@ -434,7 +435,11 @@ func (eb *Broadcaster[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE]) hand } attempt, _, _, retryable, err := eb.NewTxAttempt(ctx, *etx, eb.lggr) - if err != nil { + // Mark transaction as fatal if provided gas limit is set too low + if errors.Is(err, commonfee.ErrFeeLimitTooLow) { + etx.Error = null.StringFrom(commonfee.ErrFeeLimitTooLow.Error()) + return eb.saveFatallyErroredTransaction(eb.lggr, etx), false + } else if err != nil { return fmt.Errorf("processUnstartedTxs failed on NewAttempt: %w", err), retryable } diff --git a/common/types/mocks/monitoring_endpoint.go b/common/types/mocks/monitoring_endpoint.go new file mode 100644 index 0000000000..5afc04c909 --- /dev/null +++ b/common/types/mocks/monitoring_endpoint.go @@ -0,0 +1,65 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// MonitoringEndpoint is an autogenerated mock type for the MonitoringEndpoint type +type MonitoringEndpoint struct { + mock.Mock +} + +type MonitoringEndpoint_Expecter struct { + mock *mock.Mock +} + +func (_m *MonitoringEndpoint) EXPECT() *MonitoringEndpoint_Expecter { + return &MonitoringEndpoint_Expecter{mock: &_m.Mock} +} + +// SendLog provides a mock function with given fields: log +func (_m *MonitoringEndpoint) SendLog(log []byte) { + _m.Called(log) +} + +// MonitoringEndpoint_SendLog_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendLog' +type MonitoringEndpoint_SendLog_Call struct { + *mock.Call +} + +// SendLog is a helper method to define mock.On call +// - log []byte +func (_e *MonitoringEndpoint_Expecter) SendLog(log interface{}) *MonitoringEndpoint_SendLog_Call { + return &MonitoringEndpoint_SendLog_Call{Call: _e.mock.On("SendLog", log)} +} + +func (_c *MonitoringEndpoint_SendLog_Call) Run(run func(log []byte)) *MonitoringEndpoint_SendLog_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *MonitoringEndpoint_SendLog_Call) Return() *MonitoringEndpoint_SendLog_Call { + _c.Call.Return() + return _c +} + +func (_c *MonitoringEndpoint_SendLog_Call) RunAndReturn(run func([]byte)) *MonitoringEndpoint_SendLog_Call { + _c.Call.Return(run) + return _c +} + +// NewMonitoringEndpoint creates a new instance of MonitoringEndpoint. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMonitoringEndpoint(t interface { + mock.TestingT + Cleanup(func()) +}) *MonitoringEndpoint { + mock := &MonitoringEndpoint{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/contracts/gas-snapshots/ccip.gas-snapshot b/contracts/gas-snapshots/ccip.gas-snapshot index 7e79a922d3..8c84c95305 100644 --- a/contracts/gas-snapshots/ccip.gas-snapshot +++ b/contracts/gas-snapshots/ccip.gas-snapshot @@ -103,7 +103,7 @@ CommitStore_report:test_OnlyPriceUpdateStaleReport_Revert() (gas: 59049) CommitStore_report:test_OnlyTokenPriceUpdates_Success() (gas: 53251) CommitStore_report:test_Paused_Revert() (gas: 21259) CommitStore_report:test_ReportAndPriceUpdate_Success() (gas: 84242) -CommitStore_report:test_ReportOnlyRootSuccess_gas() (gas: 56313) +CommitStore_report:test_ReportOnlyRootSuccess_gas() (gas: 56249) CommitStore_report:test_RootAlreadyCommitted_Revert() (gas: 63969) CommitStore_report:test_StaleReportWithRoot_Success() (gas: 119420) CommitStore_report:test_Unhealthy_Revert() (gas: 44751) @@ -123,13 +123,13 @@ CommitStore_verify:test_Paused_Revert() (gas: 18496) CommitStore_verify:test_TooManyLeaves_Revert() (gas: 36785) DefensiveExampleTest:test_HappyPath_Success() (gas: 200018) DefensiveExampleTest:test_Recovery() (gas: 424256) -E2E:test_E2E_3MessagesSuccess_gas() (gas: 1104303) +E2E:test_E2E_3MessagesSuccess_gas() (gas: 1133785) EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_NotACompatiblePool_Revert() (gas: 37797) EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_Success() (gas: 103733) -EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_TokenHandlingError_transfer_Revert() (gas: 85258) +EVM2EVMOffRamp__releaseOrMintToken:test__releaseOrMintToken_TokenHandlingError_transfer_Revert() (gas: 82758) EVM2EVMOffRamp__releaseOrMintToken:test_releaseOrMintToken_InvalidDataLength_Revert() (gas: 36786) EVM2EVMOffRamp__releaseOrMintToken:test_releaseOrMintToken_ReleaseOrMintBalanceMismatch_Revert() (gas: 94302) -EVM2EVMOffRamp__releaseOrMintToken:test_releaseOrMintToken_TokenHandlingError_BalanceOf_Revert() (gas: 39768) +EVM2EVMOffRamp__releaseOrMintToken:test_releaseOrMintToken_TokenHandlingError_BalanceOf_Revert() (gas: 37268) EVM2EVMOffRamp__releaseOrMintToken:test_releaseOrMintToken_skip_ReleaseOrMintBalanceMismatch_if_pool_Revert() (gas: 86559) EVM2EVMOffRamp__releaseOrMintTokens:test_OverValueWithARLOff_Success() (gas: 385308) EVM2EVMOffRamp__releaseOrMintTokens:test_PriceNotFoundForToken_Reverts() (gas: 141902) @@ -162,13 +162,13 @@ EVM2EVMOffRamp_execute:test_RootNotCommitted_Revert() (gas: 42539) EVM2EVMOffRamp_execute:test_SingleMessageNoTokensUnordered_Success() (gas: 157347) EVM2EVMOffRamp_execute:test_SingleMessageNoTokens_Success() (gas: 172692) EVM2EVMOffRamp_execute:test_SingleMessageToNonCCIPReceiver_Success() (gas: 247069) -EVM2EVMOffRamp_execute:test_SingleMessagesNoTokensSuccess_gas() (gas: 114153) +EVM2EVMOffRamp_execute:test_SingleMessagesNoTokensSuccess_gas() (gas: 113842) EVM2EVMOffRamp_execute:test_SkippedIncorrectNonceStillExecutes_Success() (gas: 407507) EVM2EVMOffRamp_execute:test_SkippedIncorrectNonce_Success() (gas: 54096) EVM2EVMOffRamp_execute:test_StrictUntouchedToSuccess_Success() (gas: 131047) EVM2EVMOffRamp_execute:test_TokenDataMismatch_Revert() (gas: 52090) EVM2EVMOffRamp_execute:test_TwoMessagesWithTokensAndGE_Success() (gas: 563497) -EVM2EVMOffRamp_execute:test_TwoMessagesWithTokensSuccess_gas() (gas: 495620) +EVM2EVMOffRamp_execute:test_TwoMessagesWithTokensSuccess_gas() (gas: 494294) EVM2EVMOffRamp_execute:test_UnexpectedTokenData_Revert() (gas: 35383) EVM2EVMOffRamp_execute:test_Unhealthy_Revert() (gas: 544753) EVM2EVMOffRamp_execute:test_UnsupportedNumberOfTokens_Revert() (gas: 64326) @@ -178,7 +178,7 @@ EVM2EVMOffRamp_execute:test_execute_RouterYULCall_Success() (gas: 427337) EVM2EVMOffRamp_executeSingleMessage:test_MessageSender_Revert() (gas: 18502) EVM2EVMOffRamp_executeSingleMessage:test_NonContractWithTokens_Success() (gas: 278153) EVM2EVMOffRamp_executeSingleMessage:test_NonContract_Success() (gas: 18659) -EVM2EVMOffRamp_executeSingleMessage:test_TokenHandlingError_Revert() (gas: 221449) +EVM2EVMOffRamp_executeSingleMessage:test_TokenHandlingError_Revert() (gas: 223949) EVM2EVMOffRamp_executeSingleMessage:test_ZeroGasDONExecution_Revert() (gas: 47881) EVM2EVMOffRamp_executeSingleMessage:test_executeSingleMessage_NoTokens_Success() (gas: 47352) EVM2EVMOffRamp_executeSingleMessage:test_executeSingleMessage_WithTokens_Success() (gas: 313973) @@ -230,21 +230,21 @@ EVM2EVMOnRamp_forwardFromRouter:test_MaxFeeBalanceReached_Revert() (gas: 36457) EVM2EVMOnRamp_forwardFromRouter:test_MessageGasLimitTooHigh_Revert() (gas: 29015) EVM2EVMOnRamp_forwardFromRouter:test_MessageTooLarge_Revert() (gas: 107571) EVM2EVMOnRamp_forwardFromRouter:test_OriginalSender_Revert() (gas: 22679) -EVM2EVMOnRamp_forwardFromRouter:test_OverValueWithARLOff_Success() (gas: 224625) +EVM2EVMOnRamp_forwardFromRouter:test_OverValueWithARLOff_Success() (gas: 227125) EVM2EVMOnRamp_forwardFromRouter:test_Paused_Revert() (gas: 53072) EVM2EVMOnRamp_forwardFromRouter:test_Permissions_Revert() (gas: 25481) EVM2EVMOnRamp_forwardFromRouter:test_PriceNotFoundForToken_Revert() (gas: 59347) EVM2EVMOnRamp_forwardFromRouter:test_ShouldIncrementNonceOnlyOnOrdered_Success() (gas: 179148) EVM2EVMOnRamp_forwardFromRouter:test_ShouldIncrementSeqNumAndNonce_Success() (gas: 177430) EVM2EVMOnRamp_forwardFromRouter:test_ShouldStoreNonLinkFees() (gas: 137322) -EVM2EVMOnRamp_forwardFromRouter:test_SourceTokenDataTooLarge_Revert() (gas: 3822827) +EVM2EVMOnRamp_forwardFromRouter:test_SourceTokenDataTooLarge_Revert() (gas: 3825327) EVM2EVMOnRamp_forwardFromRouter:test_TooManyTokens_Revert() (gas: 30187) EVM2EVMOnRamp_forwardFromRouter:test_Unhealthy_Revert() (gas: 43300) EVM2EVMOnRamp_forwardFromRouter:test_UnsupportedToken_Revert() (gas: 109283) EVM2EVMOnRamp_forwardFromRouter:test_ZeroAddressReceiver_Revert() (gas: 312579) EVM2EVMOnRamp_forwardFromRouter:test_forwardFromRouter_ShouldStoreLinkFees_Success() (gas: 112322) EVM2EVMOnRamp_forwardFromRouter:test_forwardFromRouter_UnsupportedToken_Revert() (gas: 72206) -EVM2EVMOnRamp_forwardFromRouter:test_forwardFromRouter_correctSourceTokenData_Success() (gas: 710531) +EVM2EVMOnRamp_forwardFromRouter:test_forwardFromRouter_correctSourceTokenData_Success() (gas: 713031) EVM2EVMOnRamp_forwardFromRouter_upgrade:test_V2NonceNewSenderStartsAtZero_Success() (gas: 147664) EVM2EVMOnRamp_forwardFromRouter_upgrade:test_V2NonceStartsAtV1Nonce_Success() (gas: 190529) EVM2EVMOnRamp_forwardFromRouter_upgrade:test_V2SenderNoncesReadsPreviousRamp_Success() (gas: 121320) @@ -275,7 +275,7 @@ EVM2EVMOnRamp_getTokenTransferCost:test__getTokenTransferCost_selfServeUsesDefau EVM2EVMOnRamp_linkAvailableForPayment:test_InsufficientLinkBalance_Success() (gas: 32615) EVM2EVMOnRamp_linkAvailableForPayment:test_LinkAvailableForPayment_Success() (gas: 134833) EVM2EVMOnRamp_payNops:test_AdminPayNops_Success() (gas: 143159) -EVM2EVMOnRamp_payNops:test_InsufficientBalance_Revert() (gas: 26543) +EVM2EVMOnRamp_payNops:test_InsufficientBalance_Revert() (gas: 29043) EVM2EVMOnRamp_payNops:test_NoFeesToPay_Revert() (gas: 127367) EVM2EVMOnRamp_payNops:test_NoNopsToPay_Revert() (gas: 133251) EVM2EVMOnRamp_payNops:test_NopPayNops_Success() (gas: 146446) @@ -307,7 +307,7 @@ EVM2EVMOnRamp_withdrawNonLinkFees:test_SettlingBalance_Success() (gas: 272035) EVM2EVMOnRamp_withdrawNonLinkFees:test_WithdrawNonLinkFees_Success() (gas: 53446) EVM2EVMOnRamp_withdrawNonLinkFees:test_WithdrawToZeroAddress_Revert() (gas: 12830) EtherSenderReceiverTest_ccipReceive:test_ccipReceive_fallbackToWethTransfer() (gas: 96729) -EtherSenderReceiverTest_ccipReceive:test_ccipReceive_happyPath() (gas: 47688) +EtherSenderReceiverTest_ccipReceive:test_ccipReceive_happyPath() (gas: 49688) EtherSenderReceiverTest_ccipReceive:test_ccipReceive_wrongToken() (gas: 17384) EtherSenderReceiverTest_ccipReceive:test_ccipReceive_wrongTokenAmount() (gas: 15677) EtherSenderReceiverTest_ccipSend:test_ccipSend_reverts_insufficientFee_feeToken() (gas: 99741) @@ -360,9 +360,9 @@ MerkleMultiProofTest:test_MerkleRootSingleLeaf_Success() (gas: 3649) MerkleMultiProofTest:test_SpecSync_gas() (gas: 34123) MockRouterTest:test_ccipSendWithInsufficientNativeTokens_Revert() (gas: 33965) MockRouterTest:test_ccipSendWithInvalidMsgValue_Revert() (gas: 60758) -MockRouterTest:test_ccipSendWithLinkFeeTokenAndValidMsgValue_Success() (gas: 126294) +MockRouterTest:test_ccipSendWithLinkFeeTokenAndValidMsgValue_Success() (gas: 126354) MockRouterTest:test_ccipSendWithLinkFeeTokenbutInsufficientAllowance_Revert() (gas: 63302) -MockRouterTest:test_ccipSendWithSufficientNativeFeeTokens_Success() (gas: 43853) +MockRouterTest:test_ccipSendWithSufficientNativeFeeTokens_Success() (gas: 43913) MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_MultipleConfigsBothLanes_Success() (gas: 132031) MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_MultipleConfigs_Success() (gas: 312057) MultiAggregateRateLimiter_applyRateLimiterConfigUpdates:test_OnlyCallableByOwner_Revert() (gas: 17717) @@ -388,7 +388,7 @@ MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithRateLimitExce MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithRateLimitReset_Success() (gas: 78780) MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithTokensOnDifferentChains_Success() (gas: 312061) MultiAggregateRateLimiter_onInboundMessage:test_ValidateMessageWithTokens_Success() (gas: 54784) -MultiAggregateRateLimiter_onOutboundMessage:test_RateLimitValueDifferentLanes_Success() (gas: 9223372036854754743) +MultiAggregateRateLimiter_onOutboundMessage:test_RateLimitValueDifferentLanes_Success() (gas: 53241) MultiAggregateRateLimiter_onOutboundMessage:test_ValidateMessageWithNoTokens_Success() (gas: 19104) MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageFromUnauthorizedCaller_Revert() (gas: 15778) MultiAggregateRateLimiter_onOutboundMessage:test_onOutboundMessage_ValidateMessageWithDifferentTokensOnDifferentChains_Success() (gas: 213732) @@ -430,16 +430,16 @@ MultiOCR3Base_transmit:test_InsufficientSignatures_Revert() (gas: 76930) MultiOCR3Base_transmit:test_NonUniqueSignature_Revert() (gas: 66127) MultiOCR3Base_transmit:test_SignatureOutOfRegistration_Revert() (gas: 33419) MultiOCR3Base_transmit:test_TooManySignatures_Revert() (gas: 79521) -MultiOCR3Base_transmit:test_TransmitSigners_gas_Success() (gas: 34131) +MultiOCR3Base_transmit:test_TransmitSigners_gas_Success() (gas: 34020) MultiOCR3Base_transmit:test_TransmitWithExtraCalldataArgs_Revert() (gas: 47114) MultiOCR3Base_transmit:test_TransmitWithLessCalldataArgs_Revert() (gas: 25682) -MultiOCR3Base_transmit:test_TransmitWithoutSignatureVerification_gas_Success() (gas: 18726) +MultiOCR3Base_transmit:test_TransmitWithoutSignatureVerification_gas_Success() (gas: 18714) MultiOCR3Base_transmit:test_UnAuthorizedTransmitter_Revert() (gas: 24191) MultiOCR3Base_transmit:test_UnauthorizedSigner_Revert() (gas: 61409) MultiOCR3Base_transmit:test_UnconfiguredPlugin_Revert() (gas: 39890) MultiOCR3Base_transmit:test_ZeroSignatures_Revert() (gas: 32973) MultiOnRampTokenPoolReentrancy:test_OnRampTokenPoolReentrancy_Success() (gas: 393335) -MultiRampsE2E:test_E2E_3MessagesMMultiOffRampSuccess_gas() (gas: 1448084) +MultiRampsE2E:test_E2E_3MessagesMMultiOffRampSuccess_gas() (gas: 1461081) NonceManager_NonceIncrementation:test_getIncrementedOutboundNonce_Success() (gas: 37907) NonceManager_NonceIncrementation:test_incrementInboundNonce_Skip() (gas: 23694) NonceManager_NonceIncrementation:test_incrementInboundNonce_Success() (gas: 38763) @@ -469,7 +469,7 @@ OCR2BaseNoChecks_setOCR2Config:test_TooManyTransmitter_Revert() (gas: 36938) OCR2BaseNoChecks_setOCR2Config:test_TransmitterCannotBeZeroAddress_Revert() (gas: 24158) OCR2BaseNoChecks_transmit:test_ConfigDigestMismatch_Revert() (gas: 17448) OCR2BaseNoChecks_transmit:test_ForkedChain_Revert() (gas: 26726) -OCR2BaseNoChecks_transmit:test_TransmitSuccess_gas() (gas: 27478) +OCR2BaseNoChecks_transmit:test_TransmitSuccess_gas() (gas: 27466) OCR2BaseNoChecks_transmit:test_UnAuthorizedTransmitter_Revert() (gas: 21296) OCR2Base_setOCR2Config:test_FMustBePositive_Revert() (gas: 12189) OCR2Base_setOCR2Config:test_FTooHigh_Revert() (gas: 12345) @@ -483,16 +483,16 @@ OCR2Base_transmit:test_ConfigDigestMismatch_Revert() (gas: 19623) OCR2Base_transmit:test_ForkedChain_Revert() (gas: 37683) OCR2Base_transmit:test_NonUniqueSignature_Revert() (gas: 55309) OCR2Base_transmit:test_SignatureOutOfRegistration_Revert() (gas: 20962) -OCR2Base_transmit:test_Transmit2SignersSuccess_gas() (gas: 51686) +OCR2Base_transmit:test_Transmit2SignersSuccess_gas() (gas: 51674) OCR2Base_transmit:test_UnAuthorizedTransmitter_Revert() (gas: 23484) OCR2Base_transmit:test_UnauthorizedSigner_Revert() (gas: 39665) OCR2Base_transmit:test_WrongNumberOfSignatures_Revert() (gas: 20557) OffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_NotACompatiblePool_Revert() (gas: 38408) OffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_Success() (gas: 106250) -OffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_TokenHandlingError_transfer_Revert() (gas: 87409) +OffRamp__releaseOrMintSingleToken:test__releaseOrMintSingleToken_TokenHandlingError_transfer_Revert() (gas: 84909) OffRamp__releaseOrMintSingleToken:test_releaseOrMintToken_InvalidDataLength_Revert() (gas: 38954) OffRamp__releaseOrMintSingleToken:test_releaseOrMintToken_ReleaseOrMintBalanceMismatch_Revert() (gas: 96511) -OffRamp__releaseOrMintSingleToken:test_releaseOrMintToken_TokenHandlingError_BalanceOf_Revert() (gas: 41956) +OffRamp__releaseOrMintSingleToken:test_releaseOrMintToken_TokenHandlingError_BalanceOf_Revert() (gas: 39456) OffRamp__releaseOrMintSingleToken:test_releaseOrMintToken_skip_ReleaseOrMintBalanceMismatch_if_pool_Revert() (gas: 88684) OffRamp_applySourceChainConfigUpdates:test_AddMultipleChains_Success() (gas: 468115) OffRamp_applySourceChainConfigUpdates:test_AddNewChain_Success() (gas: 99227) @@ -550,7 +550,7 @@ OffRamp_execute:test_ZeroReports_Revert() (gas: 17159) OffRamp_executeSingleMessage:test_MessageSender_Revert() (gas: 18190) OffRamp_executeSingleMessage:test_NonContractWithTokens_Success() (gas: 246556) OffRamp_executeSingleMessage:test_NonContract_Success() (gas: 20472) -OffRamp_executeSingleMessage:test_TokenHandlingError_Revert() (gas: 205195) +OffRamp_executeSingleMessage:test_TokenHandlingError_Revert() (gas: 207695) OffRamp_executeSingleMessage:test_ZeroGasDONExecution_Revert() (gas: 48734) OffRamp_executeSingleMessage:test_executeSingleMessage_NoTokens_Success() (gas: 48257) OffRamp_executeSingleMessage:test_executeSingleMessage_WithFailingValidationNoRouterCall_Revert() (gas: 229631) @@ -572,12 +572,12 @@ OffRamp_executeSingleReport:test_SingleMessageNoTokensOtherChain_Success() (gas: OffRamp_executeSingleReport:test_SingleMessageNoTokensUnordered_Success() (gas: 187191) OffRamp_executeSingleReport:test_SingleMessageNoTokens_Success() (gas: 206771) OffRamp_executeSingleReport:test_SingleMessageToNonCCIPReceiver_Success() (gas: 263519) -OffRamp_executeSingleReport:test_SingleMessagesNoTokensSuccess_gas() (gas: 138408) +OffRamp_executeSingleReport:test_SingleMessagesNoTokensSuccess_gas() (gas: 138111) OffRamp_executeSingleReport:test_SkippedIncorrectNonceStillExecutes_Success() (gas: 409328) OffRamp_executeSingleReport:test_SkippedIncorrectNonce_Success() (gas: 65876) OffRamp_executeSingleReport:test_TokenDataMismatch_Revert() (gas: 80909) OffRamp_executeSingleReport:test_TwoMessagesWithTokensAndGE_Success() (gas: 566299) -OffRamp_executeSingleReport:test_TwoMessagesWithTokensSuccess_gas() (gas: 517689) +OffRamp_executeSingleReport:test_TwoMessagesWithTokensSuccess_gas() (gas: 516604) OffRamp_executeSingleReport:test_UnexpectedTokenData_Revert() (gas: 35742) OffRamp_executeSingleReport:test_UnhealthySingleChainCurse_Revert() (gas: 517721) OffRamp_executeSingleReport:test_Unhealthy_Revert() (gas: 515089) @@ -646,10 +646,10 @@ OnRamp_forwardFromRouter:test_ShouldIncrementNonceOnlyOnOrdered_Success() (gas: OnRamp_forwardFromRouter:test_ShouldIncrementSeqNumAndNonce_Success() (gas: 205670) OnRamp_forwardFromRouter:test_ShouldStoreLinkFees() (gas: 121815) OnRamp_forwardFromRouter:test_ShouldStoreNonLinkFees() (gas: 143193) -OnRamp_forwardFromRouter:test_SourceTokenDataTooLarge_Revert() (gas: 3872608) +OnRamp_forwardFromRouter:test_SourceTokenDataTooLarge_Revert() (gas: 3875108) OnRamp_forwardFromRouter:test_UnsupportedToken_Revert() (gas: 108546) OnRamp_forwardFromRouter:test_forwardFromRouter_UnsupportedToken_Revert() (gas: 73975) -OnRamp_forwardFromRouter:test_forwardFromRouter_WithValidation_Success() (gas: 262685) +OnRamp_forwardFromRouter:test_forwardFromRouter_WithValidation_Success() (gas: 265185) OnRamp_getFee:test_EmptyMessage_Success() (gas: 104467) OnRamp_getFee:test_EnforceOutOfOrder_Revert() (gas: 74075) OnRamp_getFee:test_SingleTokenMessage_Success() (gas: 119799) @@ -662,7 +662,7 @@ OnRamp_setDynamicConfig:test_SetConfigInvalidConfig_Revert() (gas: 11112) OnRamp_setDynamicConfig:test_SetConfigOnlyOwner_Revert() (gas: 15939) OnRamp_setDynamicConfig:test_SetDynamicConfig_Success() (gas: 51996) OnRamp_withdrawFeeTokens:test_WithdrawFeeTokens_Success() (gas: 97214) -PingPong_ccipReceive:test_CcipReceive_Success() (gas: 150175) +PingPong_ccipReceive:test_CcipReceive_Success() (gas: 152675) PingPong_plumbing:test_OutOfOrderExecution_Success() (gas: 20277) PingPong_plumbing:test_Pausing_Success() (gas: 17777) PingPong_startPingPong:test_StartPingPong_With_OOO_Success() (gas: 163199) @@ -775,7 +775,7 @@ RMN_ownerUnbless:test_Unbless_Success() (gas: 74699) RMN_ownerUnvoteToCurse:test_CanBlessAndCurseAfterGlobalCurseIsLifted() (gas: 470965) RMN_ownerUnvoteToCurse:test_IsIdempotent() (gas: 397532) RMN_ownerUnvoteToCurse:test_NonOwner_Revert() (gas: 18591) -RMN_ownerUnvoteToCurse:test_OwnerUnvoteToCurseSuccess_gas() (gas: 357403) +RMN_ownerUnvoteToCurse:test_OwnerUnvoteToCurseSuccess_gas() (gas: 357400) RMN_ownerUnvoteToCurse:test_UnknownVoter_Revert() (gas: 32980) RMN_ownerUnvoteToCurse_Benchmark:test_OwnerUnvoteToCurse_1Voter_LiftsCurse_gas() (gas: 261985) RMN_permaBlessing:test_PermaBlessing() (gas: 202686) @@ -783,7 +783,7 @@ RMN_setConfig:test_BlessVoterIsZeroAddress_Revert() (gas: 15494) RMN_setConfig:test_EitherThresholdIsZero_Revert() (gas: 21095) RMN_setConfig:test_NonOwner_Revert() (gas: 14713) RMN_setConfig:test_RepeatedAddress_Revert() (gas: 18213) -RMN_setConfig:test_SetConfigSuccess_gas() (gas: 104204) +RMN_setConfig:test_SetConfigSuccess_gas() (gas: 104022) RMN_setConfig:test_TotalWeightsSmallerThanEachThreshold_Revert() (gas: 30173) RMN_setConfig:test_VoteToBlessByEjectedVoter_Revert() (gas: 130303) RMN_setConfig:test_VotersLengthIsZero_Revert() (gas: 12128) @@ -868,18 +868,18 @@ Router_getFee:test_GetFeeSupportedChain_Success() (gas: 46464) Router_getFee:test_UnsupportedDestinationChain_Revert() (gas: 17138) Router_getSupportedTokens:test_GetSupportedTokens_Revert() (gas: 10460) Router_recoverTokens:test_RecoverTokensInvalidRecipient_Revert() (gas: 11316) -Router_recoverTokens:test_RecoverTokensNoFunds_Revert() (gas: 17761) +Router_recoverTokens:test_RecoverTokensNoFunds_Revert() (gas: 20261) Router_recoverTokens:test_RecoverTokensNonOwner_Revert() (gas: 11159) Router_recoverTokens:test_RecoverTokensValueReceiver_Revert() (gas: 422138) -Router_recoverTokens:test_RecoverTokens_Success() (gas: 50437) +Router_recoverTokens:test_RecoverTokens_Success() (gas: 52437) Router_routeMessage:test_AutoExec_Success() (gas: 42684) Router_routeMessage:test_ExecutionEvent_Success() (gas: 157980) Router_routeMessage:test_ManualExec_Success() (gas: 35381) Router_routeMessage:test_OnlyOffRamp_Revert() (gas: 25116) Router_routeMessage:test_WhenNotHealthy_Revert() (gas: 44724) Router_setWrappedNative:test_OnlyOwner_Revert() (gas: 10985) -SelfFundedPingPong_ccipReceive:test_FundingIfNotANop_Revert() (gas: 53531) -SelfFundedPingPong_ccipReceive:test_Funding_Success() (gas: 417002) +SelfFundedPingPong_ccipReceive:test_FundingIfNotANop_Revert() (gas: 55531) +SelfFundedPingPong_ccipReceive:test_Funding_Success() (gas: 419502) SelfFundedPingPong_setCountIncrBeforeFunding:test_setCountIncrBeforeFunding() (gas: 20151) TokenAdminRegistry_acceptAdminRole:test_acceptAdminRole_OnlyPendingAdministrator_Revert() (gas: 51085) TokenAdminRegistry_acceptAdminRole:test_acceptAdminRole_Success() (gas: 43947) @@ -907,8 +907,8 @@ TokenPoolAndProxy:test_lockOrBurn_burnMint_Success() (gas: 6070353) TokenPoolAndProxy:test_lockOrBurn_burnWithFromMint_Success() (gas: 6101826) TokenPoolAndProxy:test_lockOrBurn_lockRelease_Success() (gas: 6319594) TokenPoolAndProxy:test_setPreviousPool_Success() (gas: 3387124) -TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_2() (gas: 6913796) -TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_4() (gas: 7097821) +TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_2() (gas: 6916296) +TokenPoolAndProxyMigration:test_tokenPoolMigration_Success_1_4() (gas: 7100321) TokenPoolWithAllowList_applyAllowListUpdates:test_AllowListNotEnabled_Revert() (gas: 2209837) TokenPoolWithAllowList_applyAllowListUpdates:test_OnlyOwner_Revert() (gas: 12089) TokenPoolWithAllowList_applyAllowListUpdates:test_SetAllowListSkipsZero_Success() (gas: 23324) diff --git a/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol b/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol index 050cd6d457..9181fb37c2 100644 --- a/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol +++ b/contracts/src/v0.8/ccip/test/mocks/MockRouter.sol @@ -45,8 +45,19 @@ contract MockCCIPRouter is IRouter, IRouterClient { uint256 gasLimit, address receiver ) internal returns (bool success, bytes memory retData, uint256 gasUsed) { - // Only send through the router if the receiver is a contract and implements the IAny2EVMMessageReceiver interface. - if (receiver.code.length == 0 || !receiver.supportsInterface(type(IAny2EVMMessageReceiver).interfaceId)) { + // There are three cases in which we skip calling the receiver: + // 1. If the message data is empty AND the gas limit is 0. + // This indicates a message that only transfers tokens. It is valid to only send tokens to a contract + // that supports the IAny2EVMMessageReceiver interface, but without this first check we would call the + // receiver without any gas, which would revert the transaction. + // 2. If the receiver is not a contract. + // 3. If the receiver is a contract but it does not support the IAny2EVMMessageReceiver interface. + // + // The ordering of these checks is important, as the first check is the cheapest to execute. + if ( + (message.data.length == 0 && gasLimit == 0) || receiver.code.length == 0 + || !receiver.supportsInterface(type(IAny2EVMMessageReceiver).interfaceId) + ) { return (true, "", 0); } diff --git a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go index 4e0a7162aa..1726405e22 100644 --- a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go +++ b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go @@ -590,6 +590,10 @@ func (g *TestGasEstimatorConfig) BlockHistory() evmconfig.BlockHistory { return &TestBlockHistoryConfig{} } +func (g *TestGasEstimatorConfig) FeeHistory() evmconfig.FeeHistory { + return &TestFeeHistoryConfig{} +} + func (g *TestGasEstimatorConfig) EIP1559DynamicFees() bool { return false } func (g *TestGasEstimatorConfig) LimitDefault() uint64 { return 1e6 } func (g *TestGasEstimatorConfig) BumpPercent() uint16 { return 2 } @@ -612,6 +616,7 @@ func (g *TestGasEstimatorConfig) LimitJobType() evmconfig.LimitJobType { func (g *TestGasEstimatorConfig) PriceMaxKey(addr common.Address) *assets.Wei { return assets.GWei(1) } +func (g *TestGasEstimatorConfig) EstimateLimit() bool { return false } func (e *TestEvmConfig) GasEstimator() evmconfig.GasEstimator { return &TestGasEstimatorConfig{bumpThreshold: e.BumpThreshold} @@ -637,6 +642,10 @@ func (b *TestBlockHistoryConfig) BlockHistorySize() uint16 { return 42 func (b *TestBlockHistoryConfig) EIP1559FeeCapBufferBlocks() uint16 { return 42 } func (b *TestBlockHistoryConfig) TransactionPercentile() uint16 { return 42 } +type TestFeeHistoryConfig struct { + evmconfig.FeeHistory +} + type transactionsConfig struct { evmconfig.Transactions e *TestEvmConfig diff --git a/core/chains/evm/client/chain_client.go b/core/chains/evm/client/chain_client.go index c27d294ebf..310528424d 100644 --- a/core/chains/evm/client/chain_client.go +++ b/core/chains/evm/client/chain_client.go @@ -84,6 +84,7 @@ type Client interface { SuggestGasPrice(ctx context.Context) (*big.Int, error) SuggestGasTipCap(ctx context.Context) (*big.Int, error) LatestBlockHeight(ctx context.Context) (*big.Int, error) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) @@ -353,6 +354,14 @@ func (c *chainClient) LatestFinalizedBlock(ctx context.Context) (*evmtypes.Head, return c.multiNode.LatestFinalizedBlock(ctx) } +func (c *chainClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + rpc, err := c.multiNode.SelectNodeRPC() + if err != nil { + return feeHistory, err + } + return rpc.FeeHistory(ctx, blockCount, rewardPercentiles) +} + func (c *chainClient) CheckTxValidity(ctx context.Context, from common.Address, to common.Address, data []byte) *SendError { msg := ethereum.CallMsg{ From: from, diff --git a/core/chains/evm/client/config_builder.go b/core/chains/evm/client/config_builder.go index fa702bac11..a791e9aaa4 100644 --- a/core/chains/evm/client/config_builder.go +++ b/core/chains/evm/client/config_builder.go @@ -43,7 +43,7 @@ func NewClientConfigs( deathDeclarationDelay time.Duration, noNewFinalizedHeadsThreshold time.Duration, finalizedBlockPollInterval time.Duration, - + newHeadsPollInterval time.Duration, ) (commonclient.ChainConfig, evmconfig.NodePool, []*toml.Node, error) { nodes, err := parseNodeConfigs(nodeCfgs) if err != nil { @@ -59,6 +59,7 @@ func NewClientConfigs( EnforceRepeatableRead: enforceRepeatableRead, DeathDeclarationDelay: commonconfig.MustNewDuration(deathDeclarationDelay), FinalizedBlockPollInterval: commonconfig.MustNewDuration(finalizedBlockPollInterval), + NewHeadsPollInterval: commonconfig.MustNewDuration(newHeadsPollInterval), } nodePoolCfg := &evmconfig.NodePoolConfig{C: nodePool} chainConfig := &evmconfig.EVMConfig{ @@ -79,15 +80,21 @@ func NewClientConfigs( func parseNodeConfigs(nodeCfgs []NodeConfig) ([]*toml.Node, error) { nodes := make([]*toml.Node, len(nodeCfgs)) for i, nodeCfg := range nodeCfgs { - if nodeCfg.WSURL == nil || nodeCfg.HTTPURL == nil { - return nil, fmt.Errorf("node config [%d]: missing WS or HTTP URL", i) + var wsURL, httpURL *commonconfig.URL + // wsUrl requirement will be checked in EVMConfig validation + if nodeCfg.WSURL != nil { + wsURL = commonconfig.MustParseURL(*nodeCfg.WSURL) + } + + if nodeCfg.HTTPURL == nil { + return nil, fmt.Errorf("node config [%d]: missing HTTP URL", i) } - wsUrl := commonconfig.MustParseURL(*nodeCfg.WSURL) - httpUrl := commonconfig.MustParseURL(*nodeCfg.HTTPURL) + + httpURL = commonconfig.MustParseURL(*nodeCfg.HTTPURL) node := &toml.Node{ Name: nodeCfg.Name, - WSURL: wsUrl, - HTTPURL: httpUrl, + WSURL: wsURL, + HTTPURL: httpURL, SendOnly: nodeCfg.SendOnly, Order: nodeCfg.Order, } diff --git a/core/chains/evm/client/config_builder_test.go b/core/chains/evm/client/config_builder_test.go index 403c6c2d61..22956fb018 100644 --- a/core/chains/evm/client/config_builder_test.go +++ b/core/chains/evm/client/config_builder_test.go @@ -37,9 +37,11 @@ func TestClientConfigBuilder(t *testing.T) { finalityDepth := ptr(uint32(10)) finalityTagEnabled := ptr(true) noNewHeadsThreshold := time.Second + newHeadsPollInterval := 0 * time.Second chainCfg, nodePool, nodes, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs, pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth, - finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, pollInterval) + finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, + pollInterval, newHeadsPollInterval) require.NoError(t, err) // Validate node pool configs @@ -52,6 +54,7 @@ func TestClientConfigBuilder(t *testing.T) { require.Equal(t, *enforceRepeatableRead, nodePool.EnforceRepeatableRead()) require.Equal(t, deathDeclarationDelay, nodePool.DeathDeclarationDelay()) require.Equal(t, pollInterval, nodePool.FinalizedBlockPollInterval()) + require.Equal(t, newHeadsPollInterval, nodePool.NewHeadsPollInterval()) // Validate node configs require.Equal(t, *nodeConfigs[0].Name, *nodes[0].Name) @@ -90,7 +93,7 @@ func TestNodeConfigs(t *testing.T) { require.Len(t, tomlNodes, len(nodeConfigs)) }) - t.Run("parsing missing ws url fails", func(t *testing.T) { + t.Run("ws can be optional", func(t *testing.T) { nodeConfigs := []client.NodeConfig{ { Name: ptr("foo1"), @@ -98,7 +101,7 @@ func TestNodeConfigs(t *testing.T) { }, } _, err := client.ParseTestNodeConfigs(nodeConfigs) - require.Error(t, err) + require.Nil(t, err) }) t.Run("parsing missing http url fails", func(t *testing.T) { diff --git a/core/chains/evm/client/errors.go b/core/chains/evm/client/errors.go index e7fff8d0db..812733b35f 100644 --- a/core/chains/evm/client/errors.go +++ b/core/chains/evm/client/errors.go @@ -247,7 +247,7 @@ var zkSync = ClientErrors{ } var zkEvm = ClientErrors{ - TerminallyStuck: regexp.MustCompile(`(?:: |^)not enough .* counters to continue the execution$`), + TerminallyStuck: regexp.MustCompile(`(?:: |^)(?:not enough .* counters to continue the execution|out of counters at node level (?:.*))$`), } var aStar = ClientErrors{ @@ -257,6 +257,7 @@ var aStar = ClientErrors{ var mantle = ClientErrors{ InsufficientEth: regexp.MustCompile(`(: |^)'*insufficient funds for gas \* price \+ value`), Fatal: regexp.MustCompile(`(: |^)'*invalid sender`), + NonceTooLow: regexp.MustCompile(`(: |^)'*nonce too low`), } var gnosis = ClientErrors{ diff --git a/core/chains/evm/client/errors_test.go b/core/chains/evm/client/errors_test.go index bddc7dd3db..e4a9900d4a 100644 --- a/core/chains/evm/client/errors_test.go +++ b/core/chains/evm/client/errors_test.go @@ -44,6 +44,7 @@ func Test_Eth_Errors(t *testing.T) { {"call failed: OldNonce, Current nonce: 22, nonce of rejected tx: 17", true, "Nethermind"}, {"nonce too low. allowed nonce range: 427 - 447, actual: 426", true, "zkSync"}, {"client error nonce too low", true, "tomlConfig"}, + {"failed to forward tx to sequencer, please try again. Error message: 'nonce too low'", true, "Mantle"}, } for _, test := range tests { @@ -215,6 +216,7 @@ func Test_Eth_Errors(t *testing.T) { {"insufficient funds for gas + value. balance: 42719769622667482000, fee: 48098250000000, value: 42719769622667482000", true, "celo"}, {"client error insufficient eth", true, "tomlConfig"}, {"transaction would cause overdraft", true, "Geth"}, + {"failed to forward tx to sequencer, please try again. Error message: 'insufficient funds for gas * price + value'", true, "Mantle"}, } for _, test := range tests { err = evmclient.NewSendErrorS(test.message) @@ -316,6 +318,8 @@ func Test_Eth_Errors(t *testing.T) { {"failed to add tx to the pool: not enough step counters to continue the execution", true, "Xlayer"}, {"failed to add tx to the pool: not enough keccak counters to continue the execution", true, "zkEVM"}, {"failed to add tx to the pool: not enough keccak counters to continue the execution", true, "Xlayer"}, + {"RPC error response: failed to add tx to the pool: out of counters at node level (Steps)", true, "zkEVM"}, + {"RPC error response: failed to add tx to the pool: out of counters at node level (GasUsed, KeccakHashes, PoseidonHashes, PoseidonPaddings, MemAligns, Arithmetics, Binaries, Steps, Sha256Hashes)", true, "Xlayer"}, } for _, test := range tests { @@ -400,6 +404,8 @@ func Test_Eth_Errors_Fatal(t *testing.T) { {"Failed to serialize transaction: max priority fee per gas higher than 2^64-1", true, "zkSync"}, {"Failed to serialize transaction: oversized data. max: 1000000; actual: 1000000", true, "zkSync"}, + {"failed to forward tx to sequencer, please try again. Error message: 'invalid sender'", true, "Mantle"}, + {"client error fatal", true, "tomlConfig"}, } diff --git a/core/chains/evm/client/evm_client.go b/core/chains/evm/client/evm_client.go index 1fd533d6aa..c596bbc3a9 100644 --- a/core/chains/evm/client/evm_client.go +++ b/core/chains/evm/client/evm_client.go @@ -15,22 +15,25 @@ import ( ) func NewEvmClient(cfg evmconfig.NodePool, chainCfg commonclient.ChainConfig, clientErrors evmconfig.ClientErrors, lggr logger.Logger, chainID *big.Int, nodes []*toml.Node, chainType chaintype.ChainType) Client { - var empty url.URL var primaries []commonclient.Node[*big.Int, *evmtypes.Head, RPCClient] var sendonlys []commonclient.SendOnlyNode[*big.Int, RPCClient] largePayloadRPCTimeout, defaultRPCTimeout := getRPCTimeouts(chainType) for i, node := range nodes { + var ws *url.URL + if node.WSURL != nil { + ws = (*url.URL)(node.WSURL) + } if node.SendOnly != nil && *node.SendOnly { - rpc := NewRPCClient(lggr, empty, (*url.URL)(node.HTTPURL), *node.Name, int32(i), chainID, - commonclient.Secondary, cfg.FinalizedBlockPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType) + rpc := NewRPCClient(lggr, nil, (*url.URL)(node.HTTPURL), *node.Name, i, chainID, + commonclient.Secondary, cfg.FinalizedBlockPollInterval(), cfg.NewHeadsPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType) sendonly := commonclient.NewSendOnlyNode(lggr, (url.URL)(*node.HTTPURL), *node.Name, chainID, rpc) sendonlys = append(sendonlys, sendonly) } else { - rpc := NewRPCClient(lggr, (url.URL)(*node.WSURL), (*url.URL)(node.HTTPURL), *node.Name, int32(i), - chainID, commonclient.Primary, cfg.FinalizedBlockPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType) + rpc := NewRPCClient(lggr, ws, (*url.URL)(node.HTTPURL), *node.Name, i, + chainID, commonclient.Primary, cfg.FinalizedBlockPollInterval(), cfg.NewHeadsPollInterval(), largePayloadRPCTimeout, defaultRPCTimeout, chainType) primaryNode := commonclient.NewNode(cfg, chainCfg, - lggr, (url.URL)(*node.WSURL), (*url.URL)(node.HTTPURL), *node.Name, int32(i), chainID, *node.Order, + lggr, ws, (*url.URL)(node.HTTPURL), *node.Name, i, chainID, *node.Order, rpc, "EVM") primaries = append(primaries, primaryNode) } diff --git a/core/chains/evm/client/evm_client_test.go b/core/chains/evm/client/evm_client_test.go index bdfcf42674..b762c14653 100644 --- a/core/chains/evm/client/evm_client_test.go +++ b/core/chains/evm/client/evm_client_test.go @@ -29,6 +29,7 @@ func TestNewEvmClient(t *testing.T) { deathDeclarationDelay := time.Second * 3 noNewFinalizedBlocksThreshold := time.Second * 5 finalizedBlockPollInterval := time.Second * 4 + newHeadsPollInterval := time.Second * 4 nodeConfigs := []client.NodeConfig{ { Name: ptr("foo"), @@ -40,7 +41,8 @@ func TestNewEvmClient(t *testing.T) { finalityTagEnabled := ptr(true) chainCfg, nodePool, nodes, err := client.NewClientConfigs(selectionMode, leaseDuration, chainTypeStr, nodeConfigs, pollFailureThreshold, pollInterval, syncThreshold, nodeIsSyncingEnabled, noNewHeadsThreshold, finalityDepth, - finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, finalizedBlockPollInterval) + finalityTagEnabled, finalizedBlockOffset, enforceRepeatableRead, deathDeclarationDelay, noNewFinalizedBlocksThreshold, + finalizedBlockPollInterval, newHeadsPollInterval) require.NoError(t, err) client := client.NewEvmClient(nodePool, chainCfg, nil, logger.Test(t), testutils.FixtureChainID, nodes, chaintype.ChainType(chainTypeStr)) diff --git a/core/chains/evm/client/helpers_test.go b/core/chains/evm/client/helpers_test.go index e996ccc5e4..328acdf5a3 100644 --- a/core/chains/evm/client/helpers_test.go +++ b/core/chains/evm/client/helpers_test.go @@ -89,6 +89,7 @@ type TestNodePoolConfig struct { NodeErrors config.ClientErrors EnforceRepeatableReadVal bool NodeDeathDeclarationDelay time.Duration + NodeNewHeadsPollInterval time.Duration } func (tc TestNodePoolConfig) PollFailureThreshold() uint32 { return tc.NodePollFailureThreshold } @@ -107,6 +108,10 @@ func (tc TestNodePoolConfig) FinalizedBlockPollInterval() time.Duration { return tc.NodeFinalizedBlockPollInterval } +func (tc TestNodePoolConfig) NewHeadsPollInterval() time.Duration { + return tc.NodeNewHeadsPollInterval +} + func (tc TestNodePoolConfig) Errors() config.ClientErrors { return tc.NodeErrors } @@ -127,7 +132,7 @@ func NewChainClientWithTestNode( rpcUrl string, rpcHTTPURL *url.URL, sendonlyRPCURLs []url.URL, - id int32, + id int, chainID *big.Int, ) (Client, error) { parsed, err := url.ParseRequestURI(rpcUrl) @@ -140,10 +145,10 @@ func NewChainClientWithTestNode( } lggr := logger.Test(t) - rpc := NewRPCClient(lggr, *parsed, rpcHTTPURL, "eth-primary-rpc-0", id, chainID, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := NewRPCClient(lggr, parsed, rpcHTTPURL, "eth-primary-rpc-0", id, chainID, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") n := commonclient.NewNode[*big.Int, *evmtypes.Head, RPCClient]( - nodeCfg, clientMocks.ChainConfig{NoNewHeadsThresholdVal: noNewHeadsThreshold}, lggr, *parsed, rpcHTTPURL, "eth-primary-node-0", id, chainID, 1, rpc, "EVM") + nodeCfg, clientMocks.ChainConfig{NoNewHeadsThresholdVal: noNewHeadsThreshold}, lggr, parsed, rpcHTTPURL, "eth-primary-node-0", id, chainID, 1, rpc, "EVM") primaries := []commonclient.Node[*big.Int, *evmtypes.Head, RPCClient]{n} var sendonlys []commonclient.SendOnlyNode[*big.Int, RPCClient] @@ -152,7 +157,7 @@ func NewChainClientWithTestNode( return nil, pkgerrors.Errorf("sendonly ethereum rpc url scheme must be http(s): %s", u.String()) } var empty url.URL - rpc := NewRPCClient(lggr, empty, &sendonlyRPCURLs[i], fmt.Sprintf("eth-sendonly-rpc-%d", i), id, chainID, commonclient.Secondary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := NewRPCClient(lggr, &empty, &sendonlyRPCURLs[i], fmt.Sprintf("eth-sendonly-rpc-%d", i), id, chainID, commonclient.Secondary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") s := commonclient.NewSendOnlyNode[*big.Int, RPCClient]( lggr, u, fmt.Sprintf("eth-sendonly-%d", i), chainID, rpc) sendonlys = append(sendonlys, s) @@ -198,7 +203,7 @@ func NewChainClientWithMockedRpc( parsed, _ := url.ParseRequestURI("ws://test") n := commonclient.NewNode[*big.Int, *evmtypes.Head, RPCClient]( - cfg, clientMocks.ChainConfig{NoNewHeadsThresholdVal: noNewHeadsThreshold}, lggr, *parsed, nil, "eth-primary-node-0", 1, chainID, 1, rpc, "EVM") + cfg, clientMocks.ChainConfig{NoNewHeadsThresholdVal: noNewHeadsThreshold}, lggr, parsed, nil, "eth-primary-node-0", 1, chainID, 1, rpc, "EVM") primaries := []commonclient.Node[*big.Int, *evmtypes.Head, RPCClient]{n} clientErrors := NewTestClientErrors() c := NewChainClient(lggr, selectionMode, leaseDuration, noNewHeadsThreshold, primaries, nil, chainID, chainType, &clientErrors, 0) diff --git a/core/chains/evm/client/mocks/client.go b/core/chains/evm/client/mocks/client.go index 7b5220033b..da034d9577 100644 --- a/core/chains/evm/client/mocks/client.go +++ b/core/chains/evm/client/mocks/client.go @@ -780,6 +780,66 @@ func (_c *Client_EstimateGas_Call) RunAndReturn(run func(context.Context, ethere return _c } +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *Client) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type Client_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *Client_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *Client_FeeHistory_Call { + return &Client_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *Client_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *Client_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *Client_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *Client_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *Client_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *Client_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // FilterLogs provides a mock function with given fields: ctx, q func (_m *Client) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { ret := _m.Called(ctx, q) diff --git a/core/chains/evm/client/mocks/rpc_client.go b/core/chains/evm/client/mocks/rpc_client.go index 06f79efd55..5567b3f897 100644 --- a/core/chains/evm/client/mocks/rpc_client.go +++ b/core/chains/evm/client/mocks/rpc_client.go @@ -889,6 +889,66 @@ func (_c *RPCClient_EstimateGas_Call) RunAndReturn(run func(context.Context, int return _c } +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *RPCClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RPCClient_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type RPCClient_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *RPCClient_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *RPCClient_FeeHistory_Call { + return &RPCClient_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *RPCClient_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *RPCClient_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *RPCClient_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *RPCClient_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *RPCClient_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *RPCClient_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // FilterEvents provides a mock function with given fields: ctx, query func (_m *RPCClient) FilterEvents(ctx context.Context, query ethereum.FilterQuery) ([]coretypes.Log, error) { ret := _m.Called(ctx, query) diff --git a/core/chains/evm/client/null_client.go b/core/chains/evm/client/null_client.go index 3129bcff9b..5b1a4d7e1b 100644 --- a/core/chains/evm/client/null_client.go +++ b/core/chains/evm/client/null_client.go @@ -235,3 +235,7 @@ func (nc *NullClient) LatestFinalizedBlock(_ context.Context) (*evmtypes.Head, e func (nc *NullClient) CheckTxValidity(_ context.Context, _ common.Address, _ common.Address, _ []byte) *SendError { return nil } + +func (nc *NullClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + return nil, nil +} diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 07aa86fc45..0bc01d715f 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -103,6 +103,7 @@ type RPCClient interface { SuggestGasTipCap(ctx context.Context) (t *big.Int, err error) TransactionReceiptGeth(ctx context.Context, txHash common.Hash) (r *types.Receipt, err error) GetInterceptedChainInfo() (latest, highestUserObservations commonclient.ChainInfo) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) } const rpcSubscriptionMethodNewHeads = "newHeads" @@ -116,18 +117,20 @@ type rawclient struct { type rpcClient struct { rpcLog logger.SugaredLogger name string - id int32 + id int chainID *big.Int tier commonclient.NodeTier largePayloadRpcTimeout time.Duration rpcTimeout time.Duration finalizedBlockPollInterval time.Duration + newHeadsPollInterval time.Duration chainType chaintype.ChainType - ws rawclient + ws *rawclient http *rawclient - stateMu sync.RWMutex // protects state* fields + stateMu sync.RWMutex // protects state* fields + subsSliceMu sync.RWMutex // protects subscription slice // Need to track subscriptions because closing the RPC does not (always?) // close the underlying subscription @@ -141,6 +144,7 @@ type rpcClient struct { // stateMu since it can happen on state transitions as well as rpcClient Close. chStopInFlight chan struct{} + chainInfoLock sync.RWMutex // intercepted values seen by callers of the rpcClient excluding health check calls. Need to ensure MultiNode provides repeatable read guarantee highestUserObservations commonclient.ChainInfo // most recent chain info observed during current lifecycle (reseted on DisconnectAll) @@ -150,13 +154,14 @@ type rpcClient struct { // NewRPCCLient returns a new *rpcClient as commonclient.RPC func NewRPCClient( lggr logger.Logger, - wsuri url.URL, + wsuri *url.URL, httpuri *url.URL, name string, - id int32, + id int, chainID *big.Int, tier commonclient.NodeTier, finalizedBlockPollInterval time.Duration, + newHeadsPollInterval time.Duration, largePayloadRpcTimeout time.Duration, rpcTimeout time.Duration, chainType chaintype.ChainType, @@ -170,8 +175,11 @@ func NewRPCClient( r.id = id r.chainID = chainID r.tier = tier - r.ws.uri = wsuri r.finalizedBlockPollInterval = finalizedBlockPollInterval + r.newHeadsPollInterval = newHeadsPollInterval + if wsuri != nil { + r.ws = &rawclient{uri: *wsuri} + } if httpuri != nil { r.http = &rawclient{uri: *httpuri} } @@ -193,30 +201,33 @@ func (r *rpcClient) Dial(callerCtx context.Context) error { ctx, cancel := r.makeQueryCtx(callerCtx, r.rpcTimeout) defer cancel() - promEVMPoolRPCNodeDials.WithLabelValues(r.chainID.String(), r.name).Inc() - lggr := r.rpcLog.With("wsuri", r.ws.uri.Redacted()) - if r.http != nil { - lggr = lggr.With("httpuri", r.http.uri.Redacted()) + if r.ws == nil && r.http == nil { + return errors.New("cannot dial rpc client when both ws and http info are missing") } - lggr.Debugw("RPC dial: evmclient.Client#dial") - wsrpc, err := rpc.DialWebsocket(ctx, r.ws.uri.String(), "") - if err != nil { - promEVMPoolRPCNodeDialsFailed.WithLabelValues(r.chainID.String(), r.name).Inc() - return r.wrapRPCClientError(pkgerrors.Wrapf(err, "error while dialing websocket: %v", r.ws.uri.Redacted())) - } + promEVMPoolRPCNodeDials.WithLabelValues(r.chainID.String(), r.name).Inc() + lggr := r.rpcLog + if r.ws != nil { + lggr = lggr.With("wsuri", r.ws.uri.Redacted()) + wsrpc, err := rpc.DialWebsocket(ctx, r.ws.uri.String(), "") + if err != nil { + promEVMPoolRPCNodeDialsFailed.WithLabelValues(r.chainID.String(), r.name).Inc() + return r.wrapRPCClientError(pkgerrors.Wrapf(err, "error while dialing websocket: %v", r.ws.uri.Redacted())) + } - r.ws.rpc = wsrpc - r.ws.geth = ethclient.NewClient(wsrpc) + r.ws.rpc = wsrpc + r.ws.geth = ethclient.NewClient(wsrpc) + } if r.http != nil { + lggr = lggr.With("httpuri", r.http.uri.Redacted()) if err := r.DialHTTP(); err != nil { return err } } + lggr.Debugw("RPC dial: evmclient.Client#dial") promEVMPoolRPCNodeDialsSuccess.WithLabelValues(r.chainID.String(), r.name).Inc() - return nil } @@ -225,7 +236,7 @@ func (r *rpcClient) Dial(callerCtx context.Context) error { // It can only return error if the URL is malformed. func (r *rpcClient) DialHTTP() error { promEVMPoolRPCNodeDials.WithLabelValues(r.chainID.String(), r.name).Inc() - lggr := r.rpcLog.With("httpuri", r.ws.uri.Redacted()) + lggr := r.rpcLog.With("httpuri", r.http.uri.Redacted()) lggr.Debugw("RPC dial: evmclient.Client#dial") var httprpc *rpc.Client @@ -245,7 +256,7 @@ func (r *rpcClient) DialHTTP() error { func (r *rpcClient) Close() { defer func() { - if r.ws.rpc != nil { + if r.ws != nil && r.ws.rpc != nil { r.ws.rpc.Close() } }() @@ -264,7 +275,10 @@ func (r *rpcClient) cancelInflightRequests() { } func (r *rpcClient) String() string { - s := fmt.Sprintf("(%s)%s:%s", r.tier.String(), r.name, r.ws.uri.Redacted()) + s := fmt.Sprintf("(%s)%s", r.tier.String(), r.name) + if r.ws != nil { + s = s + fmt.Sprintf(":%s", r.ws.uri.Redacted()) + } if r.http != nil { s = s + fmt.Sprintf(":%s", r.http.uri.Redacted()) } @@ -312,8 +326,8 @@ func (r *rpcClient) getRPCDomain() string { // registerSub adds the sub to the rpcClient list func (r *rpcClient) registerSub(sub ethereum.Subscription, stopInFLightCh chan struct{}) error { - r.stateMu.Lock() - defer r.stateMu.Unlock() + r.subsSliceMu.Lock() + defer r.subsSliceMu.Unlock() // ensure that the `sub` belongs to current life cycle of the `rpcClient` and it should not be killed due to // previous `DisconnectAll` call. select { @@ -330,13 +344,19 @@ func (r *rpcClient) registerSub(sub ethereum.Subscription, stopInFLightCh chan s // DisconnectAll disconnects all clients connected to the rpcClient func (r *rpcClient) DisconnectAll() { r.stateMu.Lock() - defer r.stateMu.Unlock() - if r.ws.rpc != nil { + if r.ws != nil && r.ws.rpc != nil { r.ws.rpc.Close() } r.cancelInflightRequests() + r.stateMu.Unlock() + + r.subsSliceMu.Lock() r.unsubscribeAll() + r.subsSliceMu.Unlock() + + r.chainInfoLock.Lock() r.latestChainInfo = commonclient.ChainInfo{} + r.chainInfoLock.Unlock() } // unsubscribeAll unsubscribes all subscriptions @@ -485,6 +505,40 @@ func (r *rpcClient) SubscribeNewHead(ctx context.Context, channel chan<- *evmtyp defer cancel() args := []interface{}{"newHeads"} lggr := r.newRqLggr().With("args", args) + if r.newHeadsPollInterval > 0 { + interval := r.newHeadsPollInterval + timeout := interval + poller, pollerCh := commonclient.NewPoller[*evmtypes.Head](interval, r.latestBlock, timeout, r.rpcLog) + if err = poller.Start(); err != nil { + return nil, err + } + + // NOTE this is a temporary special treatment for SubscribeNewHead before we refactor head tracker to use SubscribeToHeads + // as we need to forward new head from the poller channel to the channel passed from caller. + go func() { + for head := range pollerCh { + select { + case channel <- head: + // forwarding new head to the channel passed from caller + case <-poller.Err(): + // return as poller returns error + return + } + } + }() + + err = r.registerSub(&poller, chStopInFlight) + if err != nil { + return nil, err + } + + lggr.Debugf("Polling new heads over http") + return &poller, nil + } + + if ws == nil { + return nil, errors.New("SubscribeNewHead is not allowed without ws url") + } lggr.Debug("RPC call: evmclient.Client#EthSubscribe") start := time.Now() @@ -514,11 +568,32 @@ func (r *rpcClient) SubscribeNewHead(ctx context.Context, channel chan<- *evmtyp func (r *rpcClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.Head, sub commontypes.Subscription, err error) { ctx, cancel, chStopInFlight, ws, _ := r.acquireQueryCtx(ctx, r.rpcTimeout) defer cancel() - args := []interface{}{rpcSubscriptionMethodNewHeads} start := time.Now() lggr := r.newRqLggr().With("args", args) + // if new head based on http polling is enabled, we will replace it for WS newHead subscription + if r.newHeadsPollInterval > 0 { + interval := r.newHeadsPollInterval + timeout := interval + poller, channel := commonclient.NewPoller[*evmtypes.Head](interval, r.latestBlock, timeout, r.rpcLog) + if err = poller.Start(); err != nil { + return nil, nil, err + } + + err = r.registerSub(&poller, chStopInFlight) + if err != nil { + return nil, nil, err + } + + lggr.Debugf("Polling new heads over http") + return channel, &poller, nil + } + + if ws == nil { + return nil, nil, errors.New("SubscribeNewHead is not allowed without ws url") + } + lggr.Debug("RPC call: evmclient.Client#EthSubscribe") defer func() { duration := time.Since(start) @@ -546,7 +621,9 @@ func (r *rpcClient) SubscribeToHeads(ctx context.Context) (ch <-chan *evmtypes.H return channel, forwarder, err } -func (r *rpcClient) SubscribeToFinalizedHeads(_ context.Context) (<-chan *evmtypes.Head, commontypes.Subscription, error) { +func (r *rpcClient) SubscribeToFinalizedHeads(ctx context.Context) (<-chan *evmtypes.Head, commontypes.Subscription, error) { + _, cancel, chStopInFlight, _, _ := r.acquireQueryCtx(ctx, r.rpcTimeout) + defer cancel() interval := r.finalizedBlockPollInterval if interval == 0 { return nil, nil, errors.New("FinalizedBlockPollInterval is 0") @@ -556,6 +633,12 @@ func (r *rpcClient) SubscribeToFinalizedHeads(_ context.Context) (<-chan *evmtyp if err := poller.Start(); err != nil { return nil, nil, err } + + err := r.registerSub(&poller, chStopInFlight) + if err != nil { + return nil, nil, err + } + return channel, &poller, nil } @@ -596,6 +679,7 @@ func (r *rpcClient) TransactionReceiptGeth(ctx context.Context, txHash common.Ha return } + func (r *rpcClient) TransactionByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, err error) { ctx, cancel, ws, http := r.makeLiveQueryCtxAndSafeGetClients(ctx, r.rpcTimeout) defer cancel() @@ -690,6 +774,10 @@ func (r *rpcClient) LatestFinalizedBlock(ctx context.Context) (head *evmtypes.He return } +func (r *rpcClient) latestBlock(ctx context.Context) (head *evmtypes.Head, err error) { + return r.BlockByNumber(ctx, nil) +} + func (r *rpcClient) astarLatestFinalizedBlock(ctx context.Context, result interface{}) (err error) { var hashResult string err = r.CallContext(ctx, &hashResult, "chain_getFinalizedHead") @@ -1116,6 +1204,29 @@ func (r *rpcClient) BalanceAt(ctx context.Context, account common.Address, block return } +func (r *rpcClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + ctx, cancel, ws, http := r.makeLiveQueryCtxAndSafeGetClients(ctx, r.rpcTimeout) + defer cancel() + lggr := r.newRqLggr().With("blockCount", blockCount, "rewardPercentiles", rewardPercentiles) + + lggr.Debug("RPC call: evmclient.Client#FeeHistory") + start := time.Now() + if http != nil { + feeHistory, err = http.geth.FeeHistory(ctx, blockCount, nil, rewardPercentiles) + err = r.wrapHTTP(err) + } else { + feeHistory, err = ws.geth.FeeHistory(ctx, blockCount, nil, rewardPercentiles) + err = r.wrapWS(err) + } + duration := time.Since(start) + + r.logResult(lggr, err, duration, r.getRPCDomain(), "FeeHistory", + "feeHistory", feeHistory, + ) + + return +} + // CallArgs represents the data used to call the balance method of a contract. // "To" is the address of the ERC contract. "Data" is the message sent // to the contract. "From" is the sender address. @@ -1189,6 +1300,9 @@ func (r *rpcClient) ClientVersion(ctx context.Context) (version string, err erro func (r *rpcClient) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (_ ethereum.Subscription, err error) { ctx, cancel, chStopInFlight, ws, _ := r.acquireQueryCtx(ctx, r.rpcTimeout) defer cancel() + if ws == nil { + return nil, errors.New("SubscribeFilterLogs is not allowed without ws url") + } lggr := r.newRqLggr().With("q", q) lggr.Debug("RPC call: evmclient.Client#SubscribeFilterLogs") @@ -1293,18 +1407,21 @@ func (r *rpcClient) wrapHTTP(err error) error { } // makeLiveQueryCtxAndSafeGetClients wraps makeQueryCtx -func (r *rpcClient) makeLiveQueryCtxAndSafeGetClients(parentCtx context.Context, timeout time.Duration) (ctx context.Context, cancel context.CancelFunc, ws rawclient, http *rawclient) { +func (r *rpcClient) makeLiveQueryCtxAndSafeGetClients(parentCtx context.Context, timeout time.Duration) (ctx context.Context, cancel context.CancelFunc, ws *rawclient, http *rawclient) { ctx, cancel, _, ws, http = r.acquireQueryCtx(parentCtx, timeout) return } func (r *rpcClient) acquireQueryCtx(parentCtx context.Context, timeout time.Duration) (ctx context.Context, cancel context.CancelFunc, - chStopInFlight chan struct{}, ws rawclient, http *rawclient) { + chStopInFlight chan struct{}, ws *rawclient, http *rawclient) { // Need to wrap in mutex because state transition can cancel and replace the // context r.stateMu.RLock() chStopInFlight = r.chStopInFlight - ws = r.ws + if r.ws != nil { + cp := *r.ws + ws = &cp + } if r.http != nil { cp := *r.http http = &cp @@ -1379,8 +1496,8 @@ func (r *rpcClient) onNewHead(ctx context.Context, requestCh <-chan struct{}, he return } - r.stateMu.Lock() - defer r.stateMu.Unlock() + r.chainInfoLock.Lock() + defer r.chainInfoLock.Unlock() if !commonclient.CtxIsHeathCheckRequest(ctx) { r.highestUserObservations.BlockNumber = max(r.highestUserObservations.BlockNumber, head.Number) r.highestUserObservations.TotalDifficulty = commonclient.MaxTotalDifficulty(r.highestUserObservations.TotalDifficulty, head.TotalDifficulty) @@ -1398,8 +1515,8 @@ func (r *rpcClient) onNewFinalizedHead(ctx context.Context, requestCh <-chan str if head == nil { return } - r.stateMu.Lock() - defer r.stateMu.Unlock() + r.chainInfoLock.Lock() + defer r.chainInfoLock.Unlock() if !commonclient.CtxIsHeathCheckRequest(ctx) { r.highestUserObservations.FinalizedBlockNumber = max(r.highestUserObservations.FinalizedBlockNumber, head.Number) } @@ -1412,8 +1529,8 @@ func (r *rpcClient) onNewFinalizedHead(ctx context.Context, requestCh <-chan str } func (r *rpcClient) GetInterceptedChainInfo() (latest, highestUserObservations commonclient.ChainInfo) { - r.stateMu.RLock() - defer r.stateMu.RUnlock() + r.chainInfoLock.RLock() + defer r.chainInfoLock.RUnlock() return r.latestChainInfo, r.highestUserObservations } diff --git a/core/chains/evm/client/rpc_client_test.go b/core/chains/evm/client/rpc_client_test.go index 1282188099..662c757ffb 100644 --- a/core/chains/evm/client/rpc_client_test.go +++ b/core/chains/evm/client/rpc_client_test.go @@ -7,6 +7,7 @@ import ( "fmt" "math/big" "net/url" + "sync" "testing" "time" @@ -18,6 +19,8 @@ import ( "github.com/tidwall/gjson" "go.uber.org/zap" + commontypes "github.com/smartcontractkit/chainlink/v2/common/types" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -56,11 +59,37 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { } return } + + checkClosedRPCClientShouldRemoveExistingSub := func(t tests.TestingT, ctx context.Context, sub commontypes.Subscription, rpcClient client.RPCClient) { + errCh := sub.Err() + + // ensure sub exists + require.Equal(t, int32(1), rpcClient.SubscribersCount()) + rpcClient.DisconnectAll() + + // ensure sub is closed + select { + case <-errCh: // ok + default: + assert.Fail(t, "channel should be closed") + } + + require.NoError(t, rpcClient.Dial(ctx)) + require.Equal(t, int32(0), rpcClient.SubscribersCount()) + } + + t.Run("WS and HTTP URL cannot be both empty", func(t *testing.T) { + // ws is optional when LogBroadcaster is disabled, however SubscribeFilterLogs will return error if ws is missing + observedLggr, _ := logger.TestObserved(t, zap.DebugLevel) + rpcClient := client.NewRPCClient(observedLggr, nil, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + require.Equal(t, errors.New("cannot dial rpc client when both ws and http info are missing"), rpcClient.Dial(ctx)) + }) + t.Run("Updates chain info on new blocks", func(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) // set to default values @@ -110,7 +139,7 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) ch := make(chan *evmtypes.Head) @@ -130,10 +159,80 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { assert.Equal(t, int64(0), highestUserObservations.FinalizedBlockNumber) assert.Equal(t, (*big.Int)(nil), highestUserObservations.TotalDifficulty) }) + t.Run("SubscribeToHeads with http polling enabled will update new heads", func(t *testing.T) { + type rpcServer struct { + Head *evmtypes.Head + URL *url.URL + } + createRPCServer := func() *rpcServer { + server := &rpcServer{} + server.Head = &evmtypes.Head{Number: 127} + server.URL = testutils.NewWSServer(t, chainId, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + assert.Equal(t, "eth_getBlockByNumber", method) + if assert.True(t, params.IsArray()) && assert.Equal(t, "latest", params.Array()[0].String()) { + head := server.Head + jsonHead, err := json.Marshal(head) + if err != nil { + panic(fmt.Errorf("failed to marshal head: %w", err)) + } + resp.Result = string(jsonHead) + } + + return + }).WSURL() + return server + } + + server := createRPCServer() + rpc := client.NewRPCClient(lggr, server.URL, nil, "rpc", 1, chainId, commonclient.Primary, 0, tests.TestInterval, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + defer rpc.Close() + require.NoError(t, rpc.Dial(ctx)) + latest, highestUserObservations := rpc.GetInterceptedChainInfo() + // latest chain info hasn't been initialized + assert.Equal(t, int64(0), latest.BlockNumber) + assert.Equal(t, int64(0), highestUserObservations.BlockNumber) + + headCh, sub, err := rpc.SubscribeToHeads(commonclient.CtxAddHealthCheckFlag(tests.Context(t))) + require.NoError(t, err) + defer sub.Unsubscribe() + + head := <-headCh + assert.Equal(t, server.Head.Number, head.BlockNumber()) + // the http polling subscription should update the head block + latest, highestUserObservations = rpc.GetInterceptedChainInfo() + assert.Equal(t, server.Head.Number, latest.BlockNumber) + assert.Equal(t, server.Head.Number, highestUserObservations.BlockNumber) + }) + t.Run("Concurrent Unsubscribe and onNewHead calls do not lead to a deadlock", func(t *testing.T) { + const numberOfAttempts = 1000 // need a large number to increase the odds of reproducing the issue + server := testutils.NewWSServer(t, chainId, serverCallBack) + wsURL := server.WSURL() + + rpc := client.NewRPCClient(lggr, wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + defer rpc.Close() + require.NoError(t, rpc.Dial(ctx)) + var wg sync.WaitGroup + for i := 0; i < numberOfAttempts; i++ { + ch := make(chan *evmtypes.Head) + sub, err := rpc.SubscribeNewHead(tests.Context(t), ch) + require.NoError(t, err) + wg.Add(2) + go func() { + server.MustWriteBinaryMessageSync(t, makeNewHeadWSMessage(&evmtypes.Head{Number: 256, TotalDifficulty: big.NewInt(1000)})) + wg.Done() + }() + go func() { + rpc.UnsubscribeAllExceptAliveLoop() + sub.Unsubscribe() + wg.Done() + }() + wg.Wait() + } + }) t.Run("Block's chain ID matched configured", func(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) ch := make(chan *evmtypes.Head) @@ -150,17 +249,79 @@ func TestRPCClient_SubscribeNewHead(t *testing.T) { }) wsURL := server.WSURL() observedLggr, observed := logger.TestObserved(t, zap.DebugLevel) - rpc := client.NewRPCClient(observedLggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(observedLggr, wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") require.NoError(t, rpc.Dial(ctx)) server.Close() _, err := rpc.SubscribeNewHead(ctx, make(chan *evmtypes.Head)) require.ErrorContains(t, err, "RPCClient returned error (rpc)") tests.AssertLogEventually(t, observed, "evmclient.Client#EthSubscribe RPC call failure") }) + t.Run("Closed rpc client should remove existing SubscribeNewHead subscription with WS", func(t *testing.T) { + server := testutils.NewWSServer(t, chainId, serverCallBack) + wsURL := server.WSURL() + + rpc := client.NewRPCClient(lggr, wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + defer rpc.Close() + require.NoError(t, rpc.Dial(ctx)) + + ch := make(chan *evmtypes.Head) + sub, err := rpc.SubscribeNewHead(tests.Context(t), ch) + require.NoError(t, err) + checkClosedRPCClientShouldRemoveExistingSub(t, ctx, sub, rpc) + }) + t.Run("Closed rpc client should remove existing SubscribeNewHead subscription with HTTP polling", func(t *testing.T) { + server := testutils.NewWSServer(t, chainId, serverCallBack) + wsURL := server.WSURL() + + rpc := client.NewRPCClient(lggr, wsURL, &url.URL{}, "rpc", 1, chainId, commonclient.Primary, 0, 1, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + defer rpc.Close() + require.NoError(t, rpc.Dial(ctx)) + + ch := make(chan *evmtypes.Head) + sub, err := rpc.SubscribeNewHead(tests.Context(t), ch) + require.NoError(t, err) + checkClosedRPCClientShouldRemoveExistingSub(t, ctx, sub, rpc) + }) + t.Run("Closed rpc client should remove existing SubscribeToHeads subscription with WS", func(t *testing.T) { + server := testutils.NewWSServer(t, chainId, serverCallBack) + wsURL := server.WSURL() + + rpc := client.NewRPCClient(lggr, wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + defer rpc.Close() + require.NoError(t, rpc.Dial(ctx)) + + _, sub, err := rpc.SubscribeToHeads(tests.Context(t)) + require.NoError(t, err) + checkClosedRPCClientShouldRemoveExistingSub(t, ctx, sub, rpc) + }) + t.Run("Closed rpc client should remove existing SubscribeToHeads subscription with HTTP polling", func(t *testing.T) { + server := testutils.NewWSServer(t, chainId, serverCallBack) + wsURL := server.WSURL() + + rpc := client.NewRPCClient(lggr, wsURL, &url.URL{}, "rpc", 1, chainId, commonclient.Primary, 0, 1, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + defer rpc.Close() + require.NoError(t, rpc.Dial(ctx)) + + _, sub, err := rpc.SubscribeToHeads(tests.Context(t)) + require.NoError(t, err) + checkClosedRPCClientShouldRemoveExistingSub(t, ctx, sub, rpc) + }) + t.Run("Closed rpc client should remove existing SubscribeToFinalizedHeads subscription", func(t *testing.T) { + server := testutils.NewWSServer(t, chainId, serverCallBack) + wsURL := server.WSURL() + + rpc := client.NewRPCClient(lggr, wsURL, &url.URL{}, "rpc", 1, chainId, commonclient.Primary, 1, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + defer rpc.Close() + require.NoError(t, rpc.Dial(ctx)) + + _, sub, err := rpc.SubscribeToFinalizedHeads(tests.Context(t)) + require.NoError(t, err) + checkClosedRPCClientShouldRemoveExistingSub(t, ctx, sub, rpc) + }) t.Run("Subscription error is properly wrapper", func(t *testing.T) { server := testutils.NewWSServer(t, chainId, serverCallBack) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) sub, err := rpc.SubscribeNewHead(ctx, make(chan *evmtypes.Head)) @@ -182,13 +343,22 @@ func TestRPCClient_SubscribeFilterLogs(t *testing.T) { lggr := logger.Test(t) ctx, cancel := context.WithTimeout(tests.Context(t), tests.WaitTimeout(t)) defer cancel() + t.Run("Failed SubscribeFilterLogs when WSURL is empty", func(t *testing.T) { + // ws is optional when LogBroadcaster is disabled, however SubscribeFilterLogs will return error if ws is missing + observedLggr, _ := logger.TestObserved(t, zap.DebugLevel) + rpcClient := client.NewRPCClient(observedLggr, nil, &url.URL{}, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + require.Nil(t, rpcClient.Dial(ctx)) + + _, err := rpcClient.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log)) + require.Equal(t, errors.New("SubscribeFilterLogs is not allowed without ws url"), err) + }) t.Run("Failed SubscribeFilterLogs logs and returns proper error", func(t *testing.T) { server := testutils.NewWSServer(t, chainId, func(reqMethod string, reqParams gjson.Result) (resp testutils.JSONRPCResponse) { return resp }) wsURL := server.WSURL() observedLggr, observed := logger.TestObserved(t, zap.DebugLevel) - rpc := client.NewRPCClient(observedLggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(observedLggr, wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") require.NoError(t, rpc.Dial(ctx)) server.Close() _, err := rpc.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log)) @@ -205,7 +375,7 @@ func TestRPCClient_SubscribeFilterLogs(t *testing.T) { return resp }) wsURL := server.WSURL() - rpc := client.NewRPCClient(lggr, *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") defer rpc.Close() require.NoError(t, rpc.Dial(ctx)) sub, err := rpc.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, make(chan types.Log)) @@ -254,7 +424,7 @@ func TestRPCClient_LatestFinalizedBlock(t *testing.T) { } server := createRPCServer() - rpc := client.NewRPCClient(lggr, *server.URL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") + rpc := client.NewRPCClient(lggr, server.URL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, "") require.NoError(t, rpc.Dial(ctx)) defer rpc.Close() server.Head = &evmtypes.Head{Number: 128} @@ -364,7 +534,7 @@ func TestRpcClientLargePayloadTimeout(t *testing.T) { // use something unreasonably large for RPC timeout to ensure that we use largePayloadRPCTimeout const rpcTimeout = time.Hour const largePayloadRPCTimeout = tests.TestInterval - rpc := client.NewRPCClient(logger.Test(t), *rpcURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, largePayloadRPCTimeout, rpcTimeout, "") + rpc := client.NewRPCClient(logger.Test(t), rpcURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, largePayloadRPCTimeout, rpcTimeout, "") require.NoError(t, rpc.Dial(ctx)) defer rpc.Close() err := testCase.Fn(ctx, rpc) @@ -404,7 +574,7 @@ func TestAstarCustomFinality(t *testing.T) { const expectedFinalizedBlockNumber = int64(4) const expectedFinalizedBlockHash = "0x7441e97acf83f555e0deefef86db636bc8a37eb84747603412884e4df4d22804" - rpcClient := client.NewRPCClient(logger.Test(t), *wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, chaintype.ChainAstar) + rpcClient := client.NewRPCClient(logger.Test(t), wsURL, nil, "rpc", 1, chainId, commonclient.Primary, 0, 0, commonclient.QueryTimeout, commonclient.QueryTimeout, chaintype.ChainAstar) defer rpcClient.Close() err := rpcClient.Dial(tests.Context(t)) require.NoError(t, err) diff --git a/core/chains/evm/client/simulated_backend_client.go b/core/chains/evm/client/simulated_backend_client.go index 7dfd39f444..3ec1bff577 100644 --- a/core/chains/evm/client/simulated_backend_client.go +++ b/core/chains/evm/client/simulated_backend_client.go @@ -156,6 +156,10 @@ func (c *SimulatedBackendClient) LINKBalance(ctx context.Context, address common panic("not implemented") } +func (c *SimulatedBackendClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) { + panic("not implemented") +} + // TransactionReceipt returns the transaction receipt for the given transaction hash. func (c *SimulatedBackendClient) TransactionReceipt(ctx context.Context, receipt common.Hash) (*types.Receipt, error) { return c.b.TransactionReceipt(ctx, receipt) diff --git a/core/chains/evm/config/chain_scoped.go b/core/chains/evm/config/chain_scoped.go index b9b19cdc2c..de89272b5e 100644 --- a/core/chains/evm/config/chain_scoped.go +++ b/core/chains/evm/config/chain_scoped.go @@ -176,6 +176,10 @@ func (e *EVMConfig) OperatorFactoryAddress() string { return e.C.OperatorFactoryAddress.String() } +func (e *EVMConfig) LogBroadcasterEnabled() bool { + return e.C.LogBroadcasterEnabled == nil || *e.C.LogBroadcasterEnabled +} + func (e *EVMConfig) LogPrunePageSize() uint32 { return *e.C.LogPrunePageSize } diff --git a/core/chains/evm/config/chain_scoped_gas_estimator.go b/core/chains/evm/config/chain_scoped_gas_estimator.go index 689d5e38b8..54c7c36063 100644 --- a/core/chains/evm/config/chain_scoped_gas_estimator.go +++ b/core/chains/evm/config/chain_scoped_gas_estimator.go @@ -1,6 +1,8 @@ package config import ( + "time" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" @@ -36,6 +38,10 @@ func (g *gasEstimatorConfig) BlockHistory() BlockHistory { return &blockHistoryConfig{c: g.c.BlockHistory, blockDelay: g.blockDelay, bumpThreshold: g.c.BumpThreshold} } +func (g *gasEstimatorConfig) FeeHistory() FeeHistory { + return &feeHistoryConfig{c: g.c.FeeHistory} +} + func (g *gasEstimatorConfig) EIP1559DynamicFees() bool { return *g.c.EIP1559DynamicFees } @@ -108,6 +114,10 @@ func (g *gasEstimatorConfig) LimitJobType() LimitJobType { return &limitJobTypeConfig{c: g.c.LimitJobType} } +func (g *gasEstimatorConfig) EstimateLimit() bool { + return *g.c.EstimateLimit +} + type limitJobTypeConfig struct { c toml.GasLimitJobType } @@ -172,3 +182,11 @@ func (b *blockHistoryConfig) TransactionPercentile() uint16 { func (b *blockHistoryConfig) BlockDelay() uint16 { return *b.blockDelay } + +type feeHistoryConfig struct { + c toml.FeeHistoryEstimator +} + +func (u *feeHistoryConfig) CacheTimeout() time.Duration { + return u.c.CacheTimeout.Duration() +} diff --git a/core/chains/evm/config/chain_scoped_node_pool.go b/core/chains/evm/config/chain_scoped_node_pool.go index a497436648..4b1d02d148 100644 --- a/core/chains/evm/config/chain_scoped_node_pool.go +++ b/core/chains/evm/config/chain_scoped_node_pool.go @@ -38,6 +38,10 @@ func (n *NodePoolConfig) FinalizedBlockPollInterval() time.Duration { return n.C.FinalizedBlockPollInterval.Duration() } +func (n *NodePoolConfig) NewHeadsPollInterval() time.Duration { + return n.C.NewHeadsPollInterval.Duration() +} + func (n *NodePoolConfig) Errors() ClientErrors { return &clientErrorsConfig{c: n.C.Errors} } func (n *NodePoolConfig) EnforceRepeatableRead() bool { diff --git a/core/chains/evm/config/chaintype/chaintype.go b/core/chains/evm/config/chaintype/chaintype.go index 07ea620624..35dd214b1f 100644 --- a/core/chains/evm/config/chaintype/chaintype.go +++ b/core/chains/evm/config/chaintype/chaintype.go @@ -14,6 +14,7 @@ const ( ChainGnosis ChainType = "gnosis" ChainHedera ChainType = "hedera" ChainKroma ChainType = "kroma" + ChainMantle ChainType = "mantle" ChainMetis ChainType = "metis" ChainOptimismBedrock ChainType = "optimismBedrock" ChainScroll ChainType = "scroll" @@ -37,7 +38,7 @@ func (c ChainType) IsL2() bool { func (c ChainType) IsValid() bool { switch c { - case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync: + case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync: return true } return false @@ -57,6 +58,8 @@ func ChainTypeFromSlug(slug string) ChainType { return ChainHedera case "kroma": return ChainKroma + case "mantle": + return ChainMantle case "metis": return ChainMetis case "optimismBedrock": @@ -129,6 +132,7 @@ var ErrInvalidChainType = fmt.Errorf("must be one of %s or omitted", strings.Joi string(ChainGnosis), string(ChainHedera), string(ChainKroma), + string(ChainMantle), string(ChainMetis), string(ChainOptimismBedrock), string(ChainScroll), diff --git a/core/chains/evm/config/config.go b/core/chains/evm/config/config.go index 3ccdfeea8b..9ce28eb55a 100644 --- a/core/chains/evm/config/config.go +++ b/core/chains/evm/config/config.go @@ -43,6 +43,7 @@ type EVM interface { MinIncomingConfirmations() uint32 NonceAutoSync() bool OperatorFactoryAddress() string + LogBroadcasterEnabled() bool RPCDefaultBatchSize() uint32 NodeNoNewHeadsThreshold() time.Duration FinalizedBlockOffset() uint32 @@ -117,6 +118,7 @@ type AutoPurgeConfig interface { type GasEstimator interface { BlockHistory() BlockHistory + FeeHistory() FeeHistory LimitJobType() LimitJobType EIP1559DynamicFees() bool @@ -136,6 +138,7 @@ type GasEstimator interface { PriceMin() *assets.Wei Mode() string PriceMaxKey(gethcommon.Address) *assets.Wei + EstimateLimit() bool } type LimitJobType interface { @@ -157,6 +160,10 @@ type BlockHistory interface { TransactionPercentile() uint16 } +type FeeHistory interface { + CacheTimeout() time.Duration +} + type Workflow interface { FromAddress() *types.EIP55Address ForwarderAddress() *types.EIP55Address @@ -173,6 +180,7 @@ type NodePool interface { Errors() ClientErrors EnforceRepeatableRead() bool DeathDeclarationDelay() time.Duration + NewHeadsPollInterval() time.Duration } // TODO BCF-2509 does the chainscopedconfig really need the entire app config? diff --git a/core/chains/evm/config/config_test.go b/core/chains/evm/config/config_test.go index 617a1605d8..dd79f2b92f 100644 --- a/core/chains/evm/config/config_test.go +++ b/core/chains/evm/config/config_test.go @@ -205,6 +205,25 @@ func TestChainScopedConfig(t *testing.T) { assert.Equal(t, val.String(), cfg3.EVM().OperatorFactoryAddress()) }) }) + + t.Run("LogBroadcasterEnabled", func(t *testing.T) { + t.Run("turn on LogBroadcasterEnabled by default", func(t *testing.T) { + assert.Equal(t, true, cfg.EVM().LogBroadcasterEnabled()) + }) + + t.Run("verify LogBroadcasterEnabled is set correctly", func(t *testing.T) { + val := false + cfg3 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.LogBroadcasterEnabled = ptr(val) + }) + + assert.Equal(t, false, cfg3.EVM().LogBroadcasterEnabled()) + }) + + t.Run("use Noop logBroadcaster when LogBroadcaster is disabled", func(t *testing.T) { + + }) + }) } func TestChainScopedConfig_BlockHistory(t *testing.T) { @@ -220,6 +239,13 @@ func TestChainScopedConfig_BlockHistory(t *testing.T) { assert.Equal(t, uint16(1), bh.BlockDelay()) assert.Equal(t, uint16(4), bh.EIP1559FeeCapBufferBlocks()) } +func TestChainScopedConfig_FeeHistory(t *testing.T) { + t.Parallel() + cfg := testutils.NewTestChainScopedConfig(t, nil) + + u := cfg.EVM().GasEstimator().FeeHistory() + assert.Equal(t, 10*time.Second, u.CacheTimeout()) +} func TestChainScopedConfig_GasEstimator(t *testing.T) { t.Parallel() @@ -243,6 +269,7 @@ func TestChainScopedConfig_GasEstimator(t *testing.T) { assert.Equal(t, assets.GWei(100), ge.FeeCapDefault()) assert.Equal(t, assets.NewWeiI(1), ge.TipCapDefault()) assert.Equal(t, assets.NewWeiI(1), ge.TipCapMin()) + assert.Equal(t, false, ge.EstimateLimit()) } func TestChainScopedConfig_BSCDefaults(t *testing.T) { diff --git a/core/chains/evm/config/mocks/gas_estimator.go b/core/chains/evm/config/mocks/gas_estimator.go index b8e813e806..70b9c18d0b 100644 --- a/core/chains/evm/config/mocks/gas_estimator.go +++ b/core/chains/evm/config/mocks/gas_estimator.go @@ -298,6 +298,51 @@ func (_c *GasEstimator_EIP1559DynamicFees_Call) RunAndReturn(run func() bool) *G return _c } +// EstimateLimit provides a mock function with given fields: +func (_m *GasEstimator) EstimateLimit() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for EstimateLimit") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// GasEstimator_EstimateLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EstimateLimit' +type GasEstimator_EstimateLimit_Call struct { + *mock.Call +} + +// EstimateLimit is a helper method to define mock.On call +func (_e *GasEstimator_Expecter) EstimateLimit() *GasEstimator_EstimateLimit_Call { + return &GasEstimator_EstimateLimit_Call{Call: _e.mock.On("EstimateLimit")} +} + +func (_c *GasEstimator_EstimateLimit_Call) Run(run func()) *GasEstimator_EstimateLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *GasEstimator_EstimateLimit_Call) Return(_a0 bool) *GasEstimator_EstimateLimit_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *GasEstimator_EstimateLimit_Call) RunAndReturn(run func() bool) *GasEstimator_EstimateLimit_Call { + _c.Call.Return(run) + return _c +} + // FeeCapDefault provides a mock function with given fields: func (_m *GasEstimator) FeeCapDefault() *assets.Wei { ret := _m.Called() @@ -345,6 +390,53 @@ func (_c *GasEstimator_FeeCapDefault_Call) RunAndReturn(run func() *assets.Wei) return _c } +// FeeHistory provides a mock function with given fields: +func (_m *GasEstimator) FeeHistory() config.FeeHistory { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 config.FeeHistory + if rf, ok := ret.Get(0).(func() config.FeeHistory); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(config.FeeHistory) + } + } + + return r0 +} + +// GasEstimator_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type GasEstimator_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +func (_e *GasEstimator_Expecter) FeeHistory() *GasEstimator_FeeHistory_Call { + return &GasEstimator_FeeHistory_Call{Call: _e.mock.On("FeeHistory")} +} + +func (_c *GasEstimator_FeeHistory_Call) Run(run func()) *GasEstimator_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *GasEstimator_FeeHistory_Call) Return(_a0 config.FeeHistory) *GasEstimator_FeeHistory_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *GasEstimator_FeeHistory_Call) RunAndReturn(run func() config.FeeHistory) *GasEstimator_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // LimitDefault provides a mock function with given fields: func (_m *GasEstimator) LimitDefault() uint64 { ret := _m.Called() diff --git a/core/chains/evm/config/toml/config.go b/core/chains/evm/config/toml/config.go index ac7841ac49..1a3f911a8e 100644 --- a/core/chains/evm/config/toml/config.go +++ b/core/chains/evm/config/toml/config.go @@ -23,7 +23,9 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" ) -var ErrNotFound = errors.New("not found") +var ( + ErrNotFound = errors.New("not found") +) type HasEVMConfigs interface { EVMConfigs() EVMConfigs @@ -311,16 +313,38 @@ func (c *EVMConfig) ValidateConfig() (err error) { err = multierr.Append(err, commonconfig.ErrMissing{Name: "Nodes", Msg: "must have at least one node"}) } else { var hasPrimary bool - for _, n := range c.Nodes { + var logBroadcasterEnabled bool + var newHeadsPollingInterval commonconfig.Duration + if c.LogBroadcasterEnabled != nil { + logBroadcasterEnabled = *c.LogBroadcasterEnabled + } + + if c.NodePool.NewHeadsPollInterval != nil { + newHeadsPollingInterval = *c.NodePool.NewHeadsPollInterval + } + + for i, n := range c.Nodes { if n.SendOnly != nil && *n.SendOnly { continue } + hasPrimary = true - break + + // if the node is a primary node, then the WS URL is required when + // 1. LogBroadcaster is enabled + // 2. The http polling is disabled (newHeadsPollingInterval == 0) + if n.WSURL == nil || n.WSURL.IsZero() { + if logBroadcasterEnabled { + err = multierr.Append(err, commonconfig.ErrMissing{Name: "Nodes", Msg: fmt.Sprintf("%vth node (primary) must have a valid WSURL when LogBroadcaster is enabled", i)}) + } else if newHeadsPollingInterval.Duration() == 0 { + err = multierr.Append(err, commonconfig.ErrMissing{Name: "Nodes", Msg: fmt.Sprintf("%vth node (primary) must have a valid WSURL when http polling is disabled", i)}) + } + } } + if !hasPrimary { err = multierr.Append(err, commonconfig.ErrMissing{Name: "Nodes", - Msg: "must have at least one primary node with WSURL"}) + Msg: "must have at least one primary node"}) } } @@ -356,6 +380,7 @@ type Chain struct { NonceAutoSync *bool NoNewHeadsThreshold *commonconfig.Duration OperatorFactoryAddress *types.EIP55Address + LogBroadcasterEnabled *bool RPCDefaultBatchSize *uint32 RPCBlockQueryDelay *uint16 FinalizedBlockOffset *uint32 @@ -554,6 +579,7 @@ type GasEstimator struct { LimitMultiplier *decimal.Decimal LimitTransfer *uint64 LimitJobType GasLimitJobType `toml:",omitempty"` + EstimateLimit *bool BumpMin *assets.Wei BumpPercent *uint16 @@ -567,6 +593,7 @@ type GasEstimator struct { TipCapMin *assets.Wei BlockHistory BlockHistoryEstimator `toml:",omitempty"` + FeeHistory FeeHistoryEstimator `toml:",omitempty"` } func (e *GasEstimator) ValidateConfig() (err error) { @@ -641,6 +668,9 @@ func (e *GasEstimator) setFrom(f *GasEstimator) { if v := f.LimitTransfer; v != nil { e.LimitTransfer = v } + if v := f.EstimateLimit; v != nil { + e.EstimateLimit = v + } if v := f.PriceDefault; v != nil { e.PriceDefault = v } @@ -658,6 +688,7 @@ func (e *GasEstimator) setFrom(f *GasEstimator) { } e.LimitJobType.setFrom(&f.LimitJobType) e.BlockHistory.setFrom(&f.BlockHistory) + e.FeeHistory.setFrom(&f.FeeHistory) } type GasLimitJobType struct { @@ -720,6 +751,16 @@ func (e *BlockHistoryEstimator) setFrom(f *BlockHistoryEstimator) { } } +type FeeHistoryEstimator struct { + CacheTimeout *commonconfig.Duration +} + +func (u *FeeHistoryEstimator) setFrom(f *FeeHistoryEstimator) { + if v := f.CacheTimeout; v != nil { + u.CacheTimeout = v + } +} + type KeySpecificConfig []KeySpecific func (ks KeySpecificConfig) ValidateConfig() (err error) { @@ -859,6 +900,7 @@ type NodePool struct { Errors ClientErrors `toml:",omitempty"` EnforceRepeatableRead *bool DeathDeclarationDelay *commonconfig.Duration + NewHeadsPollInterval *commonconfig.Duration } func (p *NodePool) setFrom(f *NodePool) { @@ -891,6 +933,11 @@ func (p *NodePool) setFrom(f *NodePool) { if v := f.DeathDeclarationDelay; v != nil { p.DeathDeclarationDelay = v } + + if v := f.NewHeadsPollInterval; v != nil { + p.NewHeadsPollInterval = v + } + p.Errors.setFrom(&f.Errors) } @@ -939,19 +986,8 @@ func (n *Node) ValidateConfig() (err error) { err = multierr.Append(err, commonconfig.ErrEmpty{Name: "Name", Msg: "required for all nodes"}) } - var sendOnly bool - if n.SendOnly != nil { - sendOnly = *n.SendOnly - } - if n.WSURL == nil { - if !sendOnly { - err = multierr.Append(err, commonconfig.ErrMissing{Name: "WSURL", Msg: "required for primary nodes"}) - } - } else if n.WSURL.IsZero() { - if !sendOnly { - err = multierr.Append(err, commonconfig.ErrEmpty{Name: "WSURL", Msg: "required for primary nodes"}) - } - } else { + // relax the check here as WSURL can potentially be empty if LogBroadcaster is disabled (checked in EVMConfig Validation) + if n.WSURL != nil && !n.WSURL.IsZero() { switch n.WSURL.Scheme { case "ws", "wss": default: diff --git a/core/chains/evm/config/toml/defaults.go b/core/chains/evm/config/toml/defaults.go index c3f087da8c..d916754fde 100644 --- a/core/chains/evm/config/toml/defaults.go +++ b/core/chains/evm/config/toml/defaults.go @@ -155,6 +155,9 @@ func (c *Chain) SetFrom(f *Chain) { if v := f.OperatorFactoryAddress; v != nil { c.OperatorFactoryAddress = v } + if v := f.LogBroadcasterEnabled; v != nil { + c.LogBroadcasterEnabled = v + } if v := f.RPCDefaultBatchSize; v != nil { c.RPCDefaultBatchSize = v } diff --git a/core/chains/evm/config/toml/defaults/Astar_Mainnet.toml b/core/chains/evm/config/toml/defaults/Astar_Mainnet.toml index 2d356d9564..d1697d137a 100644 --- a/core/chains/evm/config/toml/defaults/Astar_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Astar_Mainnet.toml @@ -1,6 +1,6 @@ ChainID = '592' -# FinalityTag will be enabled once the custom tag is implemented -# FinalityTagEnabled = true +ChainType = 'astar' +FinalityTagEnabled = true FinalityDepth = 100 LogPollInterval = '6s' diff --git a/core/chains/evm/config/toml/defaults/BOB_Mainnet.toml b/core/chains/evm/config/toml/defaults/BOB_Mainnet.toml new file mode 100644 index 0000000000..806f7f8726 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/BOB_Mainnet.toml @@ -0,0 +1,23 @@ +ChainID = '60808' +# OP stack https://docs.gobob.xyz/learn/introduction/stack-overview#rollup-layer +ChainType = 'optimismBedrock' +# finality_depth was: ~850 +FinalityDepth = 900 +# block_time was: 2s +LogPollInterval = '5s' + +# finality_depth * block_time / 60 secs = ~30 min (finality time) +NoNewFinalizedHeadsThreshold = '35m' + +FinalityTagEnabled = true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + +[GasEstimator.FeeHistory] +# block_time was: 2s +CacheTimeout = '2s' + +[GasEstimator.BlockHistory] +BlockHistorySize = 100 diff --git a/core/chains/evm/config/toml/defaults/BOB_Testnet.toml b/core/chains/evm/config/toml/defaults/BOB_Testnet.toml new file mode 100644 index 0000000000..2b5313b5e5 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/BOB_Testnet.toml @@ -0,0 +1,23 @@ +ChainID = '808813' +# OP stack https://docs.gobob.xyz/learn/introduction/stack-overview#rollup-layer +ChainType = 'optimismBedrock' +# finality_depth was: ~850 +FinalityDepth = 900 +# block_time was: 2s +LogPollInterval = '5s' + +# finality_depth * block_time / 60 secs = ~30 min (finality time) +NoNewFinalizedHeadsThreshold = '35m' + +FinalityTagEnabled = true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + +[GasEstimator.FeeHistory] +# block_time was: 2s +CacheTimeout = '2s' + +[GasEstimator.BlockHistory] +BlockHistorySize = 100 diff --git a/core/chains/evm/config/toml/defaults/Berachain_Testnet.toml b/core/chains/evm/config/toml/defaults/Berachain_Testnet.toml new file mode 100644 index 0000000000..1014b7b8bf --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Berachain_Testnet.toml @@ -0,0 +1,19 @@ +ChainID = '80084' +# finality_depth: instant +FinalityDepth = 10 +# block_time: 5s +LogPollInterval = '10s' + +# finality_depth * block_time / 60 secs = ~0.8 min (finality time) +NoNewFinalizedHeadsThreshold = '5m' + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + +[GasEstimator.FeeHistory] +# block_time was: 5s +CacheTimeout = '5s' + +[GasEstimator.BlockHistory] +BlockHistorySize = 100 diff --git a/core/chains/evm/config/toml/defaults/Bsquared_Mainnet.toml b/core/chains/evm/config/toml/defaults/Bsquared_Mainnet.toml new file mode 100644 index 0000000000..98c17e9220 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Bsquared_Mainnet.toml @@ -0,0 +1,23 @@ +ChainID = '223' +# OP stack from questionnaire https://docs.google.com/spreadsheets/d/1l8dx1GzxEnjgwH5x3vB60FUr5iFALzPcs6W_wOAiuDs/edit?gid=625078687#gid=625078687 +ChainType = 'optimismBedrock' +# finality_depth was: ~1900 +FinalityDepth = 2000 +# block_time: ~2s +LogPollInterval = '5s' + +# finality_depth * block_time / 60 secs = ~66 min (finality time) +NoNewFinalizedHeadsThreshold = '70m' + +FinalityTagEnabled = true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + +[GasEstimator.FeeHistory] +# block_time was: 2s +CacheTimeout = '2s' + +[GasEstimator.BlockHistory] +BlockHistorySize = 100 diff --git a/core/chains/evm/config/toml/defaults/Bsquared_Testnet.toml b/core/chains/evm/config/toml/defaults/Bsquared_Testnet.toml new file mode 100644 index 0000000000..1a5c7c26eb --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Bsquared_Testnet.toml @@ -0,0 +1,23 @@ +ChainID = '1123' +# OP stack from questionnaire https://docs.google.com/spreadsheets/d/1l8dx1GzxEnjgwH5x3vB60FUr5iFALzPcs6W_wOAiuDs/edit?gid=625078687#gid=625078687 +ChainType = 'optimismBedrock' +# finality_depth was: ~1900 +FinalityDepth = 2000 +# block_time: ~2s +LogPollInterval = '5s' + +# finality_depth * block_time / 60 secs = ~66 min (finality time) +NoNewFinalizedHeadsThreshold = '70m' + +FinalityTagEnabled = true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + +[GasEstimator.FeeHistory] +# block_time was: 2s +CacheTimeout = '2s' + +[GasEstimator.BlockHistory] +BlockHistorySize = 100 diff --git a/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml b/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml index f057400d01..388b9f00b0 100644 --- a/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml @@ -1,6 +1,6 @@ # Metis is an L2 chain based on Optimism. ChainID = '1088' -ChainType = 'metis' +ChainType = 'optimismBedrock' # Sequencer offers absolute finality FinalityDepth = 10 FinalityTagEnabled = true diff --git a/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml b/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml index 286b888e1a..4ff4056c75 100644 --- a/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml +++ b/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml @@ -1,5 +1,5 @@ ChainID = '59902' -ChainType = 'metis' +ChainType = 'optimismBedrock' FinalityDepth = 10 FinalityTagEnabled = true MinIncomingConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Cardona.toml b/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Cardona.toml index cd91465dae..46ce80e29f 100644 --- a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Cardona.toml +++ b/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Cardona.toml @@ -13,12 +13,14 @@ ContractConfirmations = 1 ResendAfterThreshold = '3m' [GasEstimator] -PriceMin = '1 mwei' +Mode = 'FeeHistory' +# The FeeHistory estimator does not enforce PriceMin, setting it to 0 to not place any limits on the price +PriceMin = '0' BumpPercent = 40 -BumpMin = '20 mwei' -[GasEstimator.BlockHistory] -BlockHistorySize = 12 +[GasEstimator.FeeHistory] +# Refresh the suggested price every 4 seconds, to stay slightly below their polling rate of 5s +CacheTimeout = '4s' [HeadTracker] HistoryDepth = 2000 diff --git a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Goerli.toml b/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Goerli.toml deleted file mode 100644 index 6a9b47190f..0000000000 --- a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Goerli.toml +++ /dev/null @@ -1,24 +0,0 @@ -ChainID = '1442' -ChainType = 'zkevm' -FinalityDepth = 500 -NoNewHeadsThreshold = '12m' -MinIncomingConfirmations = 1 -LogPollInterval = '30s' -RPCDefaultBatchSize = 100 - -[OCR] -ContractConfirmations = 1 - -[Transactions] -ResendAfterThreshold = '3m' - -[GasEstimator] -PriceMin = '50 mwei' -BumpPercent = 40 -BumpMin = '20 mwei' - -[GasEstimator.BlockHistory] -BlockHistorySize = 12 - -[HeadTracker] -HistoryDepth = 2000 diff --git a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Mainnet.toml b/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Mainnet.toml index 79e0cb0fce..2fef7874d1 100644 --- a/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Polygon_Zkevm_Mainnet.toml @@ -14,12 +14,14 @@ ContractConfirmations = 1 ResendAfterThreshold = '3m' [GasEstimator] -PriceMin = '100 mwei' +Mode = 'FeeHistory' +# The FeeHistory estimator does not enforce PriceMin, setting it to 0 to not place any limits on the price +PriceMin = '0' BumpPercent = 40 -BumpMin = '100 mwei' -[GasEstimator.BlockHistory] -BlockHistorySize = 12 +[GasEstimator.FeeHistory] +# Refresh the suggested price every 4 seconds, to stay slightly below their polling rate of 5s +CacheTimeout = '4s' [HeadTracker] # Polygon suffers from a tremendous number of re-orgs, we need to set this to something very large to be conservative enough diff --git a/core/chains/evm/config/toml/defaults/Unichain_Testnet.toml b/core/chains/evm/config/toml/defaults/Unichain_Testnet.toml new file mode 100644 index 0000000000..68d18a1888 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Unichain_Testnet.toml @@ -0,0 +1,24 @@ +ChainID = '1301' +# OP stack: https://docs.unichain.org/docs/getting-started/set-up-a-node#overview +ChainType = 'optimismBedrock' +# finality_depth was: ~1900 +FinalityDepth = 2000 +# block_time was: ~1s +LogPollInterval = '5s' + +# batching_size_finalization_percentage = 30% according to the explorer batching view +# ( batching_size_finalization_percentage * finality_depth) * block_time / 60 secs = ~33 min (finality time) +NoNewFinalizedHeadsThreshold = '10m' + +FinalityTagEnabled = true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + +[GasEstimator.FeeHistory] +# block_time was: 1s +CacheTimeout = '1s' + +[GasEstimator.BlockHistory] +BlockHistorySize = 100 diff --git a/core/chains/evm/config/toml/defaults/Worldchain_Mainnet.toml b/core/chains/evm/config/toml/defaults/Worldchain_Mainnet.toml new file mode 100644 index 0000000000..d33b75b22e --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Worldchain_Mainnet.toml @@ -0,0 +1,23 @@ +ChainID = '480' +# OP stack: https://worldcoin.notion.site/World-Chain-Developer-Preview-Guide-23c94a67683f4e71986e5303ab88c9f3 +ChainType = 'optimismBedrock' +# finality_depth was: ~2400 +FinalityDepth = 2500 +# block_time was: 2s +LogPollInterval = '5s' + +# finality_depth * block_time / 60 secs = ~83 min (finality time) +NoNewFinalizedHeadsThreshold = '90m' + +FinalityTagEnabled = true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + +[GasEstimator.FeeHistory] +# block_time was: 2s +CacheTimeout = '2s' + +[GasEstimator.BlockHistory] +BlockHistorySize = 100 diff --git a/core/chains/evm/config/toml/defaults/Worldchain_Testnet.toml b/core/chains/evm/config/toml/defaults/Worldchain_Testnet.toml new file mode 100644 index 0000000000..34bc35d72b --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Worldchain_Testnet.toml @@ -0,0 +1,23 @@ +ChainID = '4801' +# OP stack: https://worldcoin.notion.site/World-Chain-Developer-Preview-Guide-23c94a67683f4e71986e5303ab88c9f3 +ChainType = 'optimismBedrock' +# finality_depth was: ~2400 +FinalityDepth = 2500 +# block_time was: 2s +LogPollInterval = '5s' + +# finality_depth * block_time / 60 secs = ~83 min (finality time) +NoNewFinalizedHeadsThreshold = '90m' + +FinalityTagEnabled = true + +[GasEstimator] +EIP1559DynamicFees = true +Mode = 'FeeHistory' + +[GasEstimator.FeeHistory] +# block_time was: 2s +CacheTimeout = '2s' + +[GasEstimator.BlockHistory] +BlockHistorySize = 100 diff --git a/core/chains/evm/config/toml/defaults/Zircuit_Mainnet.toml b/core/chains/evm/config/toml/defaults/Zircuit_Mainnet.toml new file mode 100644 index 0000000000..ec336d3efe --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Zircuit_Mainnet.toml @@ -0,0 +1,36 @@ +ChainID = '48900' +ChainType = 'optimismBedrock' +FinalityTagEnabled = true +FinalityDepth = 1000 +LinkContractAddress = '0x5D6d033B4FbD2190D99D930719fAbAcB64d2439a' +LogPollInterval = "2s" +NoNewHeadsThreshold = "40s" +NoNewFinalizedHeadsThreshold = "15m" + +[GasEstimator] +EIP1559DynamicFees = true +PriceMin = '1 wei' +BumpMin = '100 wei' + +[GasEstimator.BlockHistory] +BlockHistorySize = 24 + +[Transactions] +ResendAfterThreshold = '30s' + +[HeadTracker] +HistoryDepth = 2000 + +[NodePool] +SyncThreshold = 10 + +[OCR] +ContractConfirmations = 1 + +[OCR2.Automation] +GasLimit = 6500000 + +[Transactions.AutoPurge] +Enabled = true +Threshold = 90 # 90 blocks at 2s block time ~3 minutes +MinAttempts = 3 \ No newline at end of file diff --git a/core/chains/evm/config/toml/defaults/Zircuit_Sepolia.toml b/core/chains/evm/config/toml/defaults/Zircuit_Sepolia.toml new file mode 100644 index 0000000000..d6934d533d --- /dev/null +++ b/core/chains/evm/config/toml/defaults/Zircuit_Sepolia.toml @@ -0,0 +1,36 @@ +ChainID = '48899' +ChainType = 'optimismBedrock' +FinalityTagEnabled = true +FinalityDepth = 1000 +LinkContractAddress = '0xDEE94506570cA186BC1e3516fCf4fd719C312cCD' +LogPollInterval = "2s" +NoNewHeadsThreshold = "40s" +NoNewFinalizedHeadsThreshold = "15m" + +[GasEstimator] +EIP1559DynamicFees = true +PriceMin = '1 wei' +BumpMin = '100 wei' + +[GasEstimator.BlockHistory] +BlockHistorySize = 60 + +[Transactions] +ResendAfterThreshold = '30s' + +[HeadTracker] +HistoryDepth = 2000 + +[NodePool] +SyncThreshold = 10 + +[OCR] +ContractConfirmations = 1 + +[OCR2.Automation] +GasLimit = 6500000 + +[Transactions.AutoPurge] +Enabled = true +Threshold = 90 # 90 blocks at 2s block time ~3 minutes +MinAttempts = 3 \ No newline at end of file diff --git a/core/chains/evm/config/toml/defaults/fallback.toml b/core/chains/evm/config/toml/defaults/fallback.toml index e3136323f6..ab28753383 100644 --- a/core/chains/evm/config/toml/defaults/fallback.toml +++ b/core/chains/evm/config/toml/defaults/fallback.toml @@ -17,6 +17,7 @@ RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0' +LogBroadcasterEnabled = true [Transactions] ForwardersEnabled = false @@ -48,6 +49,7 @@ EIP1559DynamicFees = false FeeCapDefault = '100 gwei' TipCapDefault = '1' TipCapMin = '1' +EstimateLimit = false [GasEstimator.BlockHistory] BatchSize = 25 @@ -56,6 +58,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -73,6 +78,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 diff --git a/core/chains/evm/gas/fee_history_estimator.go b/core/chains/evm/gas/fee_history_estimator.go new file mode 100644 index 0000000000..ba3192be10 --- /dev/null +++ b/core/chains/evm/gas/fee_history_estimator.go @@ -0,0 +1,440 @@ +package gas + +import ( + "context" + "fmt" + "math/big" + "strconv" + "sync" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + bigmath "github.com/smartcontractkit/chainlink-common/pkg/utils/big_math" + + commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" + feetypes "github.com/smartcontractkit/chainlink/v2/common/fee/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" +) + +// metrics are thread safe +var ( + promFeeHistoryEstimatorGasPrice = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "gas_price_updater", + Help: "Sets latest gas price (in Wei)", + }, + []string{"evmChainID"}, + ) + promFeeHistoryEstimatorBaseFee = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "base_fee_updater", + Help: "Sets latest BaseFee (in Wei)", + }, + []string{"evmChainID"}, + ) + promFeeHistoryEstimatorMaxPriorityFeePerGas = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "max_priority_fee_per_gas_updater", + Help: "Sets latest MaxPriorityFeePerGas (in Wei)", + }, + []string{"evmChainID"}, + ) + promFeeHistoryEstimatorMaxFeePerGas = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "max_fee_per_gas_updater", + Help: "Sets latest MaxFeePerGas (in Wei)", + }, + []string{"evmChainID"}, + ) +) + +const ( + MinimumBumpPercentage = 10 // based on geth's spec + ConnectivityPercentile = 85 + BaseFeeBufferPercentage = 40 +) + +type FeeHistoryEstimatorConfig struct { + BumpPercent uint16 + CacheTimeout time.Duration + + EIP1559 bool + BlockHistorySize uint64 + RewardPercentile float64 +} + +type feeHistoryEstimatorClient interface { + SuggestGasPrice(ctx context.Context) (*big.Int, error) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) +} + +type FeeHistoryEstimator struct { + services.StateMachine + + client feeHistoryEstimatorClient + logger logger.Logger + config FeeHistoryEstimatorConfig + chainID *big.Int + + gasPriceMu sync.RWMutex + gasPrice *assets.Wei + + dynamicPriceMu sync.RWMutex + dynamicPrice DynamicFee + + priorityFeeThresholdMu sync.RWMutex + priorityFeeThreshold *assets.Wei + + l1Oracle rollups.L1Oracle + + wg *sync.WaitGroup + stopCh services.StopChan + refreshCh chan struct{} +} + +func NewFeeHistoryEstimator(lggr logger.Logger, client feeHistoryEstimatorClient, cfg FeeHistoryEstimatorConfig, chainID *big.Int, l1Oracle rollups.L1Oracle) *FeeHistoryEstimator { + return &FeeHistoryEstimator{ + client: client, + logger: logger.Named(lggr, "FeeHistoryEstimator"), + config: cfg, + chainID: chainID, + l1Oracle: l1Oracle, + wg: new(sync.WaitGroup), + stopCh: make(chan struct{}), + refreshCh: make(chan struct{}), + } +} + +func (f *FeeHistoryEstimator) Start(context.Context) error { + return f.StartOnce("FeeHistoryEstimator", func() error { + if f.config.BumpPercent < MinimumBumpPercentage { + return fmt.Errorf("BumpPercent: %s is less than minimum allowed percentage: %s", + strconv.FormatUint(uint64(f.config.BumpPercent), 10), strconv.Itoa(MinimumBumpPercentage)) + } + if f.config.EIP1559 && f.config.RewardPercentile > ConnectivityPercentile { + return fmt.Errorf("RewardPercentile: %s is greater than maximum allowed percentile: %s", + strconv.FormatUint(uint64(f.config.RewardPercentile), 10), strconv.Itoa(ConnectivityPercentile)) + } + f.wg.Add(1) + go f.run() + + return nil + }) +} + +func (f *FeeHistoryEstimator) Close() error { + return f.StopOnce("FeeHistoryEstimator", func() error { + close(f.stopCh) + f.wg.Wait() + return nil + }) +} + +func (f *FeeHistoryEstimator) run() { + defer f.wg.Done() + + t := services.TickerConfig{ + JitterPct: services.DefaultJitter, + }.NewTicker(f.config.CacheTimeout) + + for { + select { + case <-f.stopCh: + return + case <-f.refreshCh: + t.Reset() + case <-t.C: + if f.config.EIP1559 { + if err := f.RefreshDynamicPrice(); err != nil { + f.logger.Error(err) + } + } else { + if _, err := f.RefreshGasPrice(); err != nil { + f.logger.Error(err) + } + } + } + } +} + +// GetLegacyGas will fetch the cached gas price value. +func (f *FeeHistoryEstimator) GetLegacyGas(ctx context.Context, _ []byte, gasLimit uint64, maxPrice *assets.Wei, opts ...feetypes.Opt) (gasPrice *assets.Wei, chainSpecificGasLimit uint64, err error) { + chainSpecificGasLimit = gasLimit + if gasPrice, err = f.getGasPrice(); err != nil { + return + } + + if gasPrice.Cmp(maxPrice) > 0 { + f.logger.Warnf("estimated gas price: %s is greater than the maximum gas price configured: %s, returning the maximum price instead.", gasPrice, maxPrice) + return maxPrice, chainSpecificGasLimit, nil + } + return +} + +// RefreshGasPrice will use eth_gasPrice to fetch and cache the latest gas price from the RPC. +func (f *FeeHistoryEstimator) RefreshGasPrice() (*assets.Wei, error) { + ctx, cancel := f.stopCh.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + gasPrice, err := f.client.SuggestGasPrice(ctx) + if err != nil { + return nil, err + } + + promFeeHistoryEstimatorGasPrice.WithLabelValues(f.chainID.String()).Set(float64(gasPrice.Int64())) + + gasPriceWei := assets.NewWei(gasPrice) + + f.logger.Debugf("fetched new gas price: %v", gasPriceWei) + + f.gasPriceMu.Lock() + defer f.gasPriceMu.Unlock() + f.gasPrice = gasPriceWei + return f.gasPrice, nil +} + +func (f *FeeHistoryEstimator) getGasPrice() (*assets.Wei, error) { + f.gasPriceMu.RLock() + defer f.gasPriceMu.RUnlock() + if f.gasPrice == nil { + return f.gasPrice, fmt.Errorf("gas price not set") + } + return f.gasPrice, nil +} + +// GetDynamicFee will fetch the cached dynamic prices. +func (f *FeeHistoryEstimator) GetDynamicFee(ctx context.Context, maxPrice *assets.Wei) (fee DynamicFee, err error) { + if fee, err = f.getDynamicPrice(); err != nil { + return + } + + if fee.FeeCap.Cmp(maxPrice) > 0 { + f.logger.Warnf("estimated maxFeePerGas: %v is greater than the maximum price configured: %v, returning the maximum price instead.", + fee.FeeCap, maxPrice) + fee.FeeCap = maxPrice + if fee.TipCap.Cmp(maxPrice) > 0 { + f.logger.Warnf("estimated maxPriorityFeePerGas: %v is greater than the maximum price configured: %v, returning the maximum price instead.", + fee.TipCap, maxPrice) + fee.TipCap = maxPrice + } + } + + return +} + +// RefreshDynamicPrice uses eth_feeHistory to fetch the baseFee of the next block and the Nth maxPriorityFeePerGas percentiles +// of the past X blocks. It also fetches the highest 85th maxPriorityFeePerGas percentile of the past X blocks, which represents +// the highest percentile we're willing to pay. A buffer is added on top of the latest baseFee to catch fluctuations in the next +// blocks. On Ethereum the increase is baseFee * 1.125 per block, however in some chains that may vary. +func (f *FeeHistoryEstimator) RefreshDynamicPrice() error { + ctx, cancel := f.stopCh.CtxCancel(evmclient.ContextWithDefaultTimeout()) + defer cancel() + + // RewardPercentile will be used for maxPriorityFeePerGas estimations and connectivityPercentile to set the highest threshold for bumping. + feeHistory, err := f.client.FeeHistory(ctx, max(f.config.BlockHistorySize, 1), []float64{f.config.RewardPercentile, ConnectivityPercentile}) + if err != nil { + return err + } + + // eth_feeHistory doesn't return the latest baseFee of the range but rather the latest + 1, because it can be derived from the existing + // values. Source: https://github.com/ethereum/go-ethereum/blob/b0f66e34ca2a4ea7ae23475224451c8c9a569826/eth/gasprice/feehistory.go#L235 + // nextBlock is the latest returned + 1 to be aligned with the base fee value. + nextBaseFee := assets.NewWei(feeHistory.BaseFee[len(feeHistory.BaseFee)-1]) + nextBlock := big.NewInt(0).Add(feeHistory.OldestBlock, big.NewInt(int64(f.config.BlockHistorySize))) + + // If BlockHistorySize is 0 it means priority fees will be ignored from the calculations, so we set them to 0. + // If it's not we exclude 0 priced priority fees from the RPC response, even though some networks allow them. For empty blocks, eth_feeHistory + // returns priority fees with 0 values so it's safer to discard them in order to pick values from a more representative sample. + maxPriorityFeePerGas := assets.NewWeiI(0) + priorityFeeThresholdWei := assets.NewWeiI(0) + if f.config.BlockHistorySize > 0 { + var nonZeroRewardsLen int64 = 0 + priorityFee := big.NewInt(0) + priorityFeeThreshold := big.NewInt(0) + for _, reward := range feeHistory.Reward { + // reward needs to have values for two percentiles + if len(reward) < 2 { + return fmt.Errorf("reward size incorrect: %d", len(reward)) + } + // We'll calculate the average of non-zero priority fees + if reward[0].Cmp(big.NewInt(0)) > 0 { + priorityFee = priorityFee.Add(priorityFee, reward[0]) + nonZeroRewardsLen += 1 + } + // We take the max value for the bumping threshold + if reward[1].Cmp(big.NewInt(0)) > 0 { + priorityFeeThreshold = bigmath.Max(priorityFeeThreshold, reward[1]) + } + } + + if nonZeroRewardsLen == 0 || priorityFeeThreshold.Cmp(big.NewInt(0)) == 0 { + return nil + } + priorityFeeThresholdWei = assets.NewWei(priorityFeeThreshold) + maxPriorityFeePerGas = assets.NewWei(priorityFee.Div(priorityFee, big.NewInt(nonZeroRewardsLen))) + } + // baseFeeBufferPercentage is added on top as a safety to catch fluctuations in the next blocks. + maxFeePerGas := nextBaseFee.AddPercentage(BaseFeeBufferPercentage).Add(maxPriorityFeePerGas) + + promFeeHistoryEstimatorBaseFee.WithLabelValues(f.chainID.String()).Set(float64(nextBaseFee.Int64())) + promFeeHistoryEstimatorMaxPriorityFeePerGas.WithLabelValues(f.chainID.String()).Set(float64(maxPriorityFeePerGas.Int64())) + promFeeHistoryEstimatorMaxFeePerGas.WithLabelValues(f.chainID.String()).Set(float64(maxFeePerGas.Int64())) + + f.logger.Debugf("Fetched new dynamic prices, nextBlock#: %v - oldestBlock#: %v - nextBaseFee: %v - maxFeePerGas: %v - maxPriorityFeePerGas: %v - maxPriorityFeeThreshold: %v", + nextBlock, feeHistory.OldestBlock, nextBaseFee, maxFeePerGas, maxPriorityFeePerGas, priorityFeeThresholdWei) + + f.priorityFeeThresholdMu.Lock() + f.priorityFeeThreshold = priorityFeeThresholdWei + f.priorityFeeThresholdMu.Unlock() + + f.dynamicPriceMu.Lock() + defer f.dynamicPriceMu.Unlock() + f.dynamicPrice.FeeCap = maxFeePerGas + f.dynamicPrice.TipCap = maxPriorityFeePerGas + return nil +} + +func (f *FeeHistoryEstimator) getDynamicPrice() (fee DynamicFee, err error) { + f.dynamicPriceMu.RLock() + defer f.dynamicPriceMu.RUnlock() + if f.dynamicPrice.FeeCap == nil || f.dynamicPrice.TipCap == nil { + return fee, fmt.Errorf("dynamic price not set") + } + return f.dynamicPrice, nil +} + +// BumpLegacyGas provides a bumped gas price value by bumping the previous one by BumpPercent. +// If the original value is higher than the max price it returns an error as there is no room for bumping. +// It aggregates the market, bumped, and max gas price to provide a correct value. +func (f *FeeHistoryEstimator) BumpLegacyGas(ctx context.Context, originalGasPrice *assets.Wei, gasLimit uint64, maxPrice *assets.Wei, _ []EvmPriorAttempt) (*assets.Wei, uint64, error) { + // Sanitize original fee input + if originalGasPrice == nil || originalGasPrice.Cmp(maxPrice) >= 0 { + return nil, 0, fmt.Errorf("%w: error while retrieving original gas price: originalGasPrice: %s. Maximum price configured: %s", + commonfee.ErrBump, originalGasPrice, maxPrice) + } + + currentGasPrice, err := f.RefreshGasPrice() + if err != nil { + return nil, 0, err + } + f.IfStarted(func() { f.refreshCh <- struct{}{} }) + + bumpedGasPrice := originalGasPrice.AddPercentage(f.config.BumpPercent) + bumpedGasPrice, err = LimitBumpedFee(originalGasPrice, currentGasPrice, bumpedGasPrice, maxPrice) + if err != nil { + return nil, 0, fmt.Errorf("failed to limit gas price: %w", err) + } + + f.logger.Debugw("bumped gas price", "originalGasPrice", originalGasPrice, "marketGasPrice", currentGasPrice, "bumpedGasPrice", bumpedGasPrice) + + return bumpedGasPrice, gasLimit, nil +} + +// BumpDynamicFee provides a bumped dynamic fee by bumping the previous one by BumpPercent. +// If the original values are higher than the max price it returns an error as there is no room for bumping. If maxPriorityFeePerGas is bumped +// above the priority fee threshold then there is a good chance there is a connectivity issue and we shouldn't bump. +// Both maxFeePerGas as well as maxPriorityFeePerGas need to be bumped otherwise the RPC won't accept the transaction and throw an error. +// See: https://github.com/ethereum/go-ethereum/issues/24284 +// It aggregates the market, bumped, and max price to provide a correct value, for both maxFeePerGas as well as maxPriorityFerPergas. +func (f *FeeHistoryEstimator) BumpDynamicFee(ctx context.Context, originalFee DynamicFee, maxPrice *assets.Wei, _ []EvmPriorAttempt) (bumped DynamicFee, err error) { + // For chains that don't have a mempool there is no concept of gas bumping so we force-call RefreshDynamicPrice to update the underlying base fee value + if f.config.BlockHistorySize == 0 { + if !f.IfStarted(func() { + if refreshErr := f.RefreshDynamicPrice(); refreshErr != nil { + err = refreshErr + return + } + f.refreshCh <- struct{}{} + bumped, err = f.GetDynamicFee(ctx, maxPrice) + }) { + return bumped, fmt.Errorf("estimator not started") + } + return bumped, err + } + + // Sanitize original fee input + // According to geth's spec we need to bump both maxFeePerGas and maxPriorityFeePerGas for the new attempt to be accepted by the RPC + if originalFee.FeeCap == nil || + originalFee.TipCap == nil || + ((originalFee.TipCap.Cmp(originalFee.FeeCap)) > 0) || + (originalFee.FeeCap.Cmp(maxPrice) >= 0) { + return bumped, fmt.Errorf("%w: error while retrieving original dynamic fees: (originalFeePerGas: %s - originalPriorityFeePerGas: %s). Maximum price configured: %s", + commonfee.ErrBump, originalFee.FeeCap, originalFee.TipCap, maxPrice) + } + + currentDynamicPrice, err := f.getDynamicPrice() + if err != nil { + return + } + + bumpedMaxPriorityFeePerGas := originalFee.TipCap.AddPercentage(f.config.BumpPercent) + bumpedMaxFeePerGas := originalFee.FeeCap.AddPercentage(f.config.BumpPercent) + + bumpedMaxPriorityFeePerGas, err = LimitBumpedFee(originalFee.TipCap, currentDynamicPrice.TipCap, bumpedMaxPriorityFeePerGas, maxPrice) + if err != nil { + return bumped, fmt.Errorf("failed to limit maxPriorityFeePerGas: %w", err) + } + + priorityFeeThreshold, e := f.getPriorityFeeThreshold() + if e != nil { + return bumped, e + } + + if bumpedMaxPriorityFeePerGas.Cmp(priorityFeeThreshold) > 0 { + return bumped, fmt.Errorf("bumpedMaxPriorityFeePerGas: %s is above market's %sth percentile: %s, bumping is halted", + bumpedMaxPriorityFeePerGas, strconv.Itoa(ConnectivityPercentile), priorityFeeThreshold) + } + + bumpedMaxFeePerGas, err = LimitBumpedFee(originalFee.FeeCap, currentDynamicPrice.FeeCap, bumpedMaxFeePerGas, maxPrice) + if err != nil { + return bumped, fmt.Errorf("failed to limit maxFeePerGas: %w", err) + } + + bumpedFee := DynamicFee{FeeCap: bumpedMaxFeePerGas, TipCap: bumpedMaxPriorityFeePerGas} + f.logger.Debugw("bumped dynamic fee", "originalFee", originalFee, "marketFee", currentDynamicPrice, "bumpedFee", bumpedFee) + + return bumpedFee, nil +} + +// LimitBumpedFee selects the maximum value between the bumped attempt and the current fee, if there is one. If the result is higher than the max price it gets capped. +// Geth's implementation has a hard 10% minimum limit for the bumped values, otherwise it rejects the transaction with an error. +// See: https://github.com/ethereum/go-ethereum/blob/bff330335b94af3643ac2fb809793f77de3069d4/core/tx_list.go#L298 +// +// Note: for chains that support EIP-1559 but we still choose to send Legacy transactions to them, the limit is still enforcable due to the fact that Legacy transactions +// are treated the same way as Dynamic transactions under the hood. For chains that don't support EIP-1559 at all, the limit isn't enforcable but a 10% minimum bump percentage +// makes sense anyway. +func LimitBumpedFee(originalFee *assets.Wei, currentFee *assets.Wei, bumpedFee *assets.Wei, maxPrice *assets.Wei) (*assets.Wei, error) { + if currentFee != nil { + bumpedFee = assets.WeiMax(currentFee, bumpedFee) + } + bumpedFee = assets.WeiMin(bumpedFee, maxPrice) + + // The first check is added for the following edge case: + // If originalFee is below 10 wei, then adding the minimum bump percentage won't have any effect on the final value because of rounding down. + // Similarly for bumpedFee, it can have the exact same value as the originalFee, even if we bumped, given an originalFee of less than 10 wei + // and a small enough BumpPercent. + if bumpedFee.Cmp(originalFee) == 0 || + bumpedFee.Cmp(originalFee.AddPercentage(MinimumBumpPercentage)) < 0 { + return nil, fmt.Errorf("%w: %s is bumped less than minimum allowed percentage(%s) from originalFee: %s - maxPrice: %s", + commonfee.ErrBump, bumpedFee, strconv.Itoa(MinimumBumpPercentage), originalFee, maxPrice) + } + return bumpedFee, nil +} + +func (f *FeeHistoryEstimator) getPriorityFeeThreshold() (*assets.Wei, error) { + f.priorityFeeThresholdMu.RLock() + defer f.priorityFeeThresholdMu.RUnlock() + if f.priorityFeeThreshold == nil { + return f.priorityFeeThreshold, fmt.Errorf("priorityFeeThreshold not set") + } + return f.priorityFeeThreshold, nil +} + +func (f *FeeHistoryEstimator) Name() string { return f.logger.Name() } +func (f *FeeHistoryEstimator) L1Oracle() rollups.L1Oracle { return f.l1Oracle } +func (f *FeeHistoryEstimator) HealthReport() map[string]error { return map[string]error{f.Name(): nil} } +func (f *FeeHistoryEstimator) OnNewLongestChain(context.Context, *evmtypes.Head) {} diff --git a/core/chains/evm/gas/fee_history_estimator_test.go b/core/chains/evm/gas/fee_history_estimator_test.go new file mode 100644 index 0000000000..6e42e0e209 --- /dev/null +++ b/core/chains/evm/gas/fee_history_estimator_test.go @@ -0,0 +1,513 @@ +package gas_test + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" +) + +func TestFeeHistoryEstimatorLifecycle(t *testing.T) { + t.Parallel() + var gasLimit uint64 = 21000 + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("fails if you fetch gas price before the estimator starts", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{ + BumpPercent: 20, + RewardPercentile: 60, + EIP1559: false, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + _, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.ErrorContains(t, err, "gas price not set") + }) + + t.Run("fails to start if BumpPercent is lower than the minimum cap", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 9} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + assert.ErrorContains(t, u.Start(tests.Context(t)), "BumpPercent") + }) + + t.Run("fails to start if RewardPercentile is higher than ConnectivityPercentile in EIP-1559", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{ + BumpPercent: 20, + RewardPercentile: 99, + EIP1559: true, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + assert.ErrorContains(t, u.Start(tests.Context(t)), "RewardPercentile") + }) + + t.Run("starts if configs are correct", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil).Maybe() + + cfg := gas.FeeHistoryEstimatorConfig{ + BumpPercent: 20, + RewardPercentile: 10, + CacheTimeout: 10 * time.Second, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + err := u.Start(tests.Context(t)) + assert.NoError(t, err) + err = u.Close() + assert.NoError(t, err) + }) +} + +func TestFeeHistoryEstimatorGetLegacyGas(t *testing.T) { + t.Parallel() + + var gasLimit uint64 = 21000 + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("fetches a new gas price when first called", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + _, err := u.RefreshGasPrice() + assert.NoError(t, err) + gasPrice, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(10), gasPrice) + }) + + t.Run("will return max price if estimation exceeds it", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{} + + maxPrice := assets.NewWeiI(1) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + _, err := u.RefreshGasPrice() + assert.NoError(t, err) + gas1, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.NoError(t, err) + assert.Equal(t, maxPrice, gas1) + }) + + t.Run("fails if gas price has not been set yet", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{} + + maxPrice := assets.NewWeiI(1) + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + _, _, err := u.GetLegacyGas(tests.Context(t), nil, gasLimit, maxPrice) + assert.Error(t, err) + assert.ErrorContains(t, err, "gas price not set") + }) +} + +func TestFeeHistoryEstimatorBumpLegacyGas(t *testing.T) { + t.Parallel() + + var gasLimit uint64 = 21000 + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("bumps a previous attempt by BumpPercent", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalGasPrice := assets.NewWeiI(10) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(10), nil) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 50, CacheTimeout: 5 * time.Second} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + servicetest.RunHealthy(t, u) + gasPrice, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(15), gasPrice) + }) + + t.Run("fails if the original attempt is nil, or equal or higher than the max price", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + + cfg := gas.FeeHistoryEstimatorConfig{} + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + + var originalPrice *assets.Wei + _, _, err := u.BumpLegacyGas(tests.Context(t), originalPrice, gasLimit, maxPrice, nil) + assert.Error(t, err) + + originalPrice = assets.NewWeiI(100) + _, _, err = u.BumpLegacyGas(tests.Context(t), originalPrice, gasLimit, maxPrice, nil) + assert.Error(t, err) + }) + + t.Run("returns market gas price if bumped original fee is lower", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(80), nil).Once() + originalGasPrice := assets.NewWeiI(10) + + cfg := gas.FeeHistoryEstimatorConfig{} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + gas, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(80), gas) + }) + + t.Run("returns max gas price if bumped original fee is higher", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(1), nil).Once() + originalGasPrice := assets.NewWeiI(10) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 50} + + maxPrice := assets.NewWeiI(14) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + gas, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, maxPrice, gas) + }) + + t.Run("returns max gas price if the aggregation of max and original bumped fee is higher", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(1), nil).Once() + originalGasPrice := assets.NewWeiI(10) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 50} + + maxPrice := assets.NewWeiI(14) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + gas, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, maxPrice, gas) + }) + + t.Run("fails if the bumped gas price is lower than the minimum bump percentage", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + client.On("SuggestGasPrice", mock.Anything).Return(big.NewInt(100), nil).Once() + originalGasPrice := assets.NewWeiI(100) + + cfg := gas.FeeHistoryEstimatorConfig{BumpPercent: 20} + + // Price will be capped by the max price + maxPrice := assets.NewWeiI(101) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + _, _, err := u.BumpLegacyGas(tests.Context(t), originalGasPrice, gasLimit, maxPrice, nil) + assert.Error(t, err) + }) +} + +func TestFeeHistoryEstimatorGetDynamicFee(t *testing.T) { + t.Parallel() + + maxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("fetches a new dynamic fee when first called", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + baseFee := big.NewInt(5) + maxPriorityFeePerGas1 := big.NewInt(33) + maxPriorityFeePerGas2 := big.NewInt(20) + + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas1, big.NewInt(5)}, {maxPriorityFeePerGas2, big.NewInt(5)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee, baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + blockHistoryLength := 2 + cfg := gas.FeeHistoryEstimatorConfig{BlockHistorySize: uint64(blockHistoryLength)} + avrgPriorityFee := big.NewInt(0) + avrgPriorityFee.Add(maxPriorityFeePerGas1, maxPriorityFeePerGas2).Div(avrgPriorityFee, big.NewInt(int64(blockHistoryLength))) + maxFee := (*assets.Wei)(baseFee).AddPercentage(gas.BaseFeeBufferPercentage).Add((*assets.Wei)(avrgPriorityFee)) + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + dynamicFee, err := u.GetDynamicFee(tests.Context(t), maxPrice) + assert.NoError(t, err) + assert.Equal(t, maxFee, dynamicFee.FeeCap) + assert.Equal(t, (*assets.Wei)(avrgPriorityFee), dynamicFee.TipCap) + }) + + t.Run("fails if dynamic prices have not been set yet", func(t *testing.T) { + cfg := gas.FeeHistoryEstimatorConfig{} + + maxPrice := assets.NewWeiI(1) + u := gas.NewFeeHistoryEstimator(logger.Test(t), nil, cfg, chainID, nil) + _, err := u.GetDynamicFee(tests.Context(t), maxPrice) + assert.Error(t, err) + assert.ErrorContains(t, err, "dynamic price not set") + }) + + t.Run("will return max price if tip cap or fee cap exceed it", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + baseFee := big.NewInt(1) + maxPriorityFeePerGas := big.NewInt(3) + maxPrice := assets.NewWeiI(2) + + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(5)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{BlockHistorySize: 1} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + dynamicFee, err := u.GetDynamicFee(tests.Context(t), maxPrice) + assert.NoError(t, err) + assert.Equal(t, maxPrice, dynamicFee.FeeCap) + assert.Equal(t, maxPrice, dynamicFee.TipCap) + }) +} + +func TestFeeHistoryEstimatorBumpDynamicFee(t *testing.T) { + t.Parallel() + + globalMaxPrice := assets.NewWeiI(100) + chainID := big.NewInt(0) + + t.Run("bumps a previous attempt by BumpPercent", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + + // These values will be ignored because they are lower prices than the originalFee + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{big.NewInt(5), big.NewInt(50)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{big.NewInt(5)}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 2, + BumpPercent: 50, + } + + expectedFeeCap := originalFee.FeeCap.AddPercentage(cfg.BumpPercent) + expectedTipCap := originalFee.TipCap.AddPercentage(cfg.BumpPercent) + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + dynamicFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, expectedFeeCap, dynamicFee.FeeCap) + assert.Equal(t, expectedTipCap, dynamicFee.TipCap) + }) + + t.Run("fails if the original attempt is invalid", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + maxPrice := assets.NewWeiI(20) + cfg := gas.FeeHistoryEstimatorConfig{BlockHistorySize: 1} + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + // nil original fee + var originalFee gas.DynamicFee + _, err := u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + + // tip cap is higher than fee cap + originalFee = gas.DynamicFee{ + FeeCap: assets.NewWeiI(10), + TipCap: assets.NewWeiI(11), + } + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + + // fee cap is equal or higher to max price + originalFee = gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + }) + + t.Run("returns market prices if bumped original fee is lower", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + + // Market fees + baseFee := big.NewInt(5) + maxPriorityFeePerGas := big.NewInt(33) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(100)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + maxFee := (*assets.Wei)(baseFee).AddPercentage(gas.BaseFeeBufferPercentage).Add((*assets.Wei)(maxPriorityFeePerGas)) + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + bumpedFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, (*assets.Wei)(maxPriorityFeePerGas), bumpedFee.TipCap) + assert.Equal(t, maxFee, bumpedFee.FeeCap) + }) + + t.Run("fails if connectivity percentile value is reached", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(10), + } + + // Market fees + baseFee := big.NewInt(5) + maxPriorityFeePerGas := big.NewInt(33) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(30)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.Error(t, err) + }) + + t.Run("returns max price if the aggregation of max and original bumped fee is higher", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(18), + } + + maxPrice := assets.NewWeiI(25) + // Market fees + baseFee := big.NewInt(1) + maxPriorityFeePerGas := big.NewInt(1) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(30)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + bumpedFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, maxPrice, bumpedFee.TipCap) + assert.Equal(t, maxPrice, bumpedFee.FeeCap) + }) + + t.Run("fails if the bumped gas price is lower than the minimum bump percentage", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(20), + TipCap: assets.NewWeiI(18), + } + + maxPrice := assets.NewWeiI(21) + // Market fees + baseFee := big.NewInt(1) + maxPriorityFeePerGas := big.NewInt(1) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(30)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil).Once() + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 1, + BumpPercent: 50, + } + + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + err := u.RefreshDynamicPrice() + assert.NoError(t, err) + _, err = u.BumpDynamicFee(tests.Context(t), originalFee, maxPrice, nil) + assert.Error(t, err) + }) + + t.Run("ignores maxPriorityFeePerGas if there is no mempool and forces refetch", func(t *testing.T) { + client := mocks.NewFeeHistoryEstimatorClient(t) + originalFee := gas.DynamicFee{ + FeeCap: assets.NewWeiI(40), + TipCap: assets.NewWeiI(0), + } + + // Market fees + baseFee := big.NewInt(10) + maxPriorityFeePerGas := big.NewInt(0) + feeHistoryResult := ðereum.FeeHistory{ + OldestBlock: big.NewInt(1), + Reward: [][]*big.Int{{maxPriorityFeePerGas, big.NewInt(0)}}, // first one represents market price and second one connectivity price + BaseFee: []*big.Int{baseFee}, + GasUsedRatio: nil, + } + client.On("FeeHistory", mock.Anything, mock.Anything, mock.Anything).Return(feeHistoryResult, nil) + + cfg := gas.FeeHistoryEstimatorConfig{ + BlockHistorySize: 0, + BumpPercent: 20, + CacheTimeout: 10 * time.Second, + EIP1559: true, + } + + maxFeePerGas := assets.NewWei(baseFee).AddPercentage(gas.BaseFeeBufferPercentage) + u := gas.NewFeeHistoryEstimator(logger.Test(t), client, cfg, chainID, nil) + servicetest.RunHealthy(t, u) + bumpedFee, err := u.BumpDynamicFee(tests.Context(t), originalFee, globalMaxPrice, nil) + assert.NoError(t, err) + assert.Equal(t, assets.NewWeiI(0), (*assets.Wei)(maxPriorityFeePerGas)) + assert.Equal(t, maxFeePerGas, bumpedFee.FeeCap) + }) +} diff --git a/core/chains/evm/gas/helpers_test.go b/core/chains/evm/gas/helpers_test.go index 2c12ed426a..0c9a74a18d 100644 --- a/core/chains/evm/gas/helpers_test.go +++ b/core/chains/evm/gas/helpers_test.go @@ -157,6 +157,7 @@ type MockGasEstimatorConfig struct { FeeCapDefaultF *assets.Wei LimitMaxF uint64 ModeF string + EstimateLimitF bool } func NewMockGasConfig() *MockGasEstimatorConfig { @@ -214,3 +215,7 @@ func (m *MockGasEstimatorConfig) LimitMax() uint64 { func (m *MockGasEstimatorConfig) Mode() string { return m.ModeF } + +func (m *MockGasEstimatorConfig) EstimateLimit() bool { + return m.EstimateLimitF +} diff --git a/core/chains/evm/gas/mocks/evm_fee_estimator.go b/core/chains/evm/gas/mocks/evm_fee_estimator.go index a9adc261ce..a7deca2c63 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/chains/evm/assets" + common "github.com/ethereum/go-ethereum/common" + context "context" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" @@ -145,14 +147,14 @@ func (_c *EvmFeeEstimator_Close_Call) RunAndReturn(run func() error) *EvmFeeEsti return _c } -// GetFee provides a mock function with given fields: ctx, calldata, feeLimit, maxFeePrice, opts -func (_m *EvmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt) (gas.EvmFee, uint64, error) { +// GetFee provides a mock function with given fields: ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts +func (_m *EvmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt) (gas.EvmFee, uint64, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] } var _ca []interface{} - _ca = append(_ca, ctx, calldata, feeLimit, maxFeePrice) + _ca = append(_ca, ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -163,23 +165,23 @@ func (_m *EvmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit var r0 gas.EvmFee var r1 uint64 var r2 error - if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) (gas.EvmFee, uint64, error)); ok { - return rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (gas.EvmFee, uint64, error)); ok { + return rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) gas.EvmFee); ok { - r0 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) gas.EvmFee); ok { + r0 = rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r0 = ret.Get(0).(gas.EvmFee) } - if rf, ok := ret.Get(1).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) uint64); ok { - r1 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(1).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) uint64); ok { + r1 = rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r1 = ret.Get(1).(uint64) } - if rf, ok := ret.Get(2).(func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) error); ok { - r2 = rf(ctx, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(2).(func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) error); ok { + r2 = rf(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r2 = ret.Error(2) } @@ -197,43 +199,45 @@ type EvmFeeEstimator_GetFee_Call struct { // - calldata []byte // - feeLimit uint64 // - maxFeePrice *assets.Wei +// - fromAddress *common.Address +// - toAddress *common.Address // - opts ...types.Opt -func (_e *EvmFeeEstimator_Expecter) GetFee(ctx interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, opts ...interface{}) *EvmFeeEstimator_GetFee_Call { +func (_e *EvmFeeEstimator_Expecter) GetFee(ctx interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, fromAddress interface{}, toAddress interface{}, opts ...interface{}) *EvmFeeEstimator_GetFee_Call { return &EvmFeeEstimator_GetFee_Call{Call: _e.mock.On("GetFee", - append([]interface{}{ctx, calldata, feeLimit, maxFeePrice}, opts...)...)} + append([]interface{}{ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress}, opts...)...)} } -func (_c *EvmFeeEstimator_GetFee_Call) Run(run func(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt)) *EvmFeeEstimator_GetFee_Call { +func (_c *EvmFeeEstimator_GetFee_Call) Run(run func(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt)) *EvmFeeEstimator_GetFee_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]types.Opt, len(args)-4) - for i, a := range args[4:] { + variadicArgs := make([]types.Opt, len(args)-6) + for i, a := range args[6:] { if a != nil { variadicArgs[i] = a.(types.Opt) } } - run(args[0].(context.Context), args[1].([]byte), args[2].(uint64), args[3].(*assets.Wei), variadicArgs...) + run(args[0].(context.Context), args[1].([]byte), args[2].(uint64), args[3].(*assets.Wei), args[4].(*common.Address), args[5].(*common.Address), variadicArgs...) }) return _c } -func (_c *EvmFeeEstimator_GetFee_Call) Return(fee gas.EvmFee, chainSpecificFeeLimit uint64, err error) *EvmFeeEstimator_GetFee_Call { - _c.Call.Return(fee, chainSpecificFeeLimit, err) +func (_c *EvmFeeEstimator_GetFee_Call) Return(fee gas.EvmFee, estimatedFeeLimit uint64, err error) *EvmFeeEstimator_GetFee_Call { + _c.Call.Return(fee, estimatedFeeLimit, err) return _c } -func (_c *EvmFeeEstimator_GetFee_Call) RunAndReturn(run func(context.Context, []byte, uint64, *assets.Wei, ...types.Opt) (gas.EvmFee, uint64, error)) *EvmFeeEstimator_GetFee_Call { +func (_c *EvmFeeEstimator_GetFee_Call) RunAndReturn(run func(context.Context, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (gas.EvmFee, uint64, error)) *EvmFeeEstimator_GetFee_Call { _c.Call.Return(run) return _c } -// GetMaxCost provides a mock function with given fields: ctx, amount, calldata, feeLimit, maxFeePrice, opts -func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt) (*big.Int, error) { +// GetMaxCost provides a mock function with given fields: ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts +func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt) (*big.Int, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] } var _ca []interface{} - _ca = append(_ca, ctx, amount, calldata, feeLimit, maxFeePrice) + _ca = append(_ca, ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -243,19 +247,19 @@ func (_m *EvmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, ca var r0 *big.Int var r1 error - if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) (*big.Int, error)); ok { - return rf(ctx, amount, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (*big.Int, error)); ok { + return rf(ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) *big.Int); ok { - r0 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(0).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) *big.Int); ok { + r0 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*big.Int) } } - if rf, ok := ret.Get(1).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) error); ok { - r1 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, opts...) + if rf, ok := ret.Get(1).(func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) error); ok { + r1 = rf(ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) } else { r1 = ret.Error(1) } @@ -274,21 +278,23 @@ type EvmFeeEstimator_GetMaxCost_Call struct { // - calldata []byte // - feeLimit uint64 // - maxFeePrice *assets.Wei +// - fromAddress *common.Address +// - toAddress *common.Address // - opts ...types.Opt -func (_e *EvmFeeEstimator_Expecter) GetMaxCost(ctx interface{}, amount interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, opts ...interface{}) *EvmFeeEstimator_GetMaxCost_Call { +func (_e *EvmFeeEstimator_Expecter) GetMaxCost(ctx interface{}, amount interface{}, calldata interface{}, feeLimit interface{}, maxFeePrice interface{}, fromAddress interface{}, toAddress interface{}, opts ...interface{}) *EvmFeeEstimator_GetMaxCost_Call { return &EvmFeeEstimator_GetMaxCost_Call{Call: _e.mock.On("GetMaxCost", - append([]interface{}{ctx, amount, calldata, feeLimit, maxFeePrice}, opts...)...)} + append([]interface{}{ctx, amount, calldata, feeLimit, maxFeePrice, fromAddress, toAddress}, opts...)...)} } -func (_c *EvmFeeEstimator_GetMaxCost_Call) Run(run func(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...types.Opt)) *EvmFeeEstimator_GetMaxCost_Call { +func (_c *EvmFeeEstimator_GetMaxCost_Call) Run(run func(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress *common.Address, toAddress *common.Address, opts ...types.Opt)) *EvmFeeEstimator_GetMaxCost_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]types.Opt, len(args)-5) - for i, a := range args[5:] { + variadicArgs := make([]types.Opt, len(args)-7) + for i, a := range args[7:] { if a != nil { variadicArgs[i] = a.(types.Opt) } } - run(args[0].(context.Context), args[1].(assets.Eth), args[2].([]byte), args[3].(uint64), args[4].(*assets.Wei), variadicArgs...) + run(args[0].(context.Context), args[1].(assets.Eth), args[2].([]byte), args[3].(uint64), args[4].(*assets.Wei), args[5].(*common.Address), args[6].(*common.Address), variadicArgs...) }) return _c } @@ -298,7 +304,7 @@ func (_c *EvmFeeEstimator_GetMaxCost_Call) Return(_a0 *big.Int, _a1 error) *EvmF return _c } -func (_c *EvmFeeEstimator_GetMaxCost_Call) RunAndReturn(run func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, ...types.Opt) (*big.Int, error)) *EvmFeeEstimator_GetMaxCost_Call { +func (_c *EvmFeeEstimator_GetMaxCost_Call) RunAndReturn(run func(context.Context, assets.Eth, []byte, uint64, *assets.Wei, *common.Address, *common.Address, ...types.Opt) (*big.Int, error)) *EvmFeeEstimator_GetMaxCost_Call { _c.Call.Return(run) return _c } diff --git a/core/chains/evm/gas/mocks/fee_estimator_client.go b/core/chains/evm/gas/mocks/fee_estimator_client.go index 8e10107597..f5ca52ec92 100644 --- a/core/chains/evm/gas/mocks/fee_estimator_client.go +++ b/core/chains/evm/gas/mocks/fee_estimator_client.go @@ -241,6 +241,123 @@ func (_c *FeeEstimatorClient_ConfiguredChainID_Call) RunAndReturn(run func() *bi return _c } +// EstimateGas provides a mock function with given fields: ctx, call +func (_m *FeeEstimatorClient) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { + ret := _m.Called(ctx, call) + + if len(ret) == 0 { + panic("no return value specified for EstimateGas") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) (uint64, error)); ok { + return rf(ctx, call) + } + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) uint64); ok { + r0 = rf(ctx, call) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg) error); ok { + r1 = rf(ctx, call) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeEstimatorClient_EstimateGas_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EstimateGas' +type FeeEstimatorClient_EstimateGas_Call struct { + *mock.Call +} + +// EstimateGas is a helper method to define mock.On call +// - ctx context.Context +// - call ethereum.CallMsg +func (_e *FeeEstimatorClient_Expecter) EstimateGas(ctx interface{}, call interface{}) *FeeEstimatorClient_EstimateGas_Call { + return &FeeEstimatorClient_EstimateGas_Call{Call: _e.mock.On("EstimateGas", ctx, call)} +} + +func (_c *FeeEstimatorClient_EstimateGas_Call) Run(run func(ctx context.Context, call ethereum.CallMsg)) *FeeEstimatorClient_EstimateGas_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(ethereum.CallMsg)) + }) + return _c +} + +func (_c *FeeEstimatorClient_EstimateGas_Call) Return(_a0 uint64, _a1 error) *FeeEstimatorClient_EstimateGas_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FeeEstimatorClient_EstimateGas_Call) RunAndReturn(run func(context.Context, ethereum.CallMsg) (uint64, error)) *FeeEstimatorClient_EstimateGas_Call { + _c.Call.Return(run) + return _c +} + +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *FeeEstimatorClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeEstimatorClient_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type FeeEstimatorClient_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *FeeEstimatorClient_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *FeeEstimatorClient_FeeHistory_Call { + return &FeeEstimatorClient_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *FeeEstimatorClient_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *FeeEstimatorClient_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *FeeEstimatorClient_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *FeeEstimatorClient_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *FeeEstimatorClient_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *FeeEstimatorClient_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + // HeadByNumber provides a mock function with given fields: ctx, n func (_m *FeeEstimatorClient) HeadByNumber(ctx context.Context, n *big.Int) (*types.Head, error) { ret := _m.Called(ctx, n) @@ -300,6 +417,64 @@ func (_c *FeeEstimatorClient_HeadByNumber_Call) RunAndReturn(run func(context.Co return _c } +// SuggestGasPrice provides a mock function with given fields: ctx +func (_m *FeeEstimatorClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SuggestGasPrice") + } + + var r0 *big.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeEstimatorClient_SuggestGasPrice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SuggestGasPrice' +type FeeEstimatorClient_SuggestGasPrice_Call struct { + *mock.Call +} + +// SuggestGasPrice is a helper method to define mock.On call +// - ctx context.Context +func (_e *FeeEstimatorClient_Expecter) SuggestGasPrice(ctx interface{}) *FeeEstimatorClient_SuggestGasPrice_Call { + return &FeeEstimatorClient_SuggestGasPrice_Call{Call: _e.mock.On("SuggestGasPrice", ctx)} +} + +func (_c *FeeEstimatorClient_SuggestGasPrice_Call) Run(run func(ctx context.Context)) *FeeEstimatorClient_SuggestGasPrice_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *FeeEstimatorClient_SuggestGasPrice_Call) Return(_a0 *big.Int, _a1 error) *FeeEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FeeEstimatorClient_SuggestGasPrice_Call) RunAndReturn(run func(context.Context) (*big.Int, error)) *FeeEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(run) + return _c +} + // NewFeeEstimatorClient creates a new instance of FeeEstimatorClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFeeEstimatorClient(t interface { diff --git a/core/chains/evm/gas/mocks/fee_history_estimator_client.go b/core/chains/evm/gas/mocks/fee_history_estimator_client.go new file mode 100644 index 0000000000..7486214501 --- /dev/null +++ b/core/chains/evm/gas/mocks/fee_history_estimator_client.go @@ -0,0 +1,157 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + ethereum "github.com/ethereum/go-ethereum" + + mock "github.com/stretchr/testify/mock" +) + +// FeeHistoryEstimatorClient is an autogenerated mock type for the feeHistoryEstimatorClient type +type FeeHistoryEstimatorClient struct { + mock.Mock +} + +type FeeHistoryEstimatorClient_Expecter struct { + mock *mock.Mock +} + +func (_m *FeeHistoryEstimatorClient) EXPECT() *FeeHistoryEstimatorClient_Expecter { + return &FeeHistoryEstimatorClient_Expecter{mock: &_m.Mock} +} + +// FeeHistory provides a mock function with given fields: ctx, blockCount, rewardPercentiles +func (_m *FeeHistoryEstimatorClient) FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { + ret := _m.Called(ctx, blockCount, rewardPercentiles) + + if len(ret) == 0 { + panic("no return value specified for FeeHistory") + } + + var r0 *ethereum.FeeHistory + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)); ok { + return rf(ctx, blockCount, rewardPercentiles) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, []float64) *ethereum.FeeHistory); ok { + r0 = rf(ctx, blockCount, rewardPercentiles) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethereum.FeeHistory) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, []float64) error); ok { + r1 = rf(ctx, blockCount, rewardPercentiles) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeHistoryEstimatorClient_FeeHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeeHistory' +type FeeHistoryEstimatorClient_FeeHistory_Call struct { + *mock.Call +} + +// FeeHistory is a helper method to define mock.On call +// - ctx context.Context +// - blockCount uint64 +// - rewardPercentiles []float64 +func (_e *FeeHistoryEstimatorClient_Expecter) FeeHistory(ctx interface{}, blockCount interface{}, rewardPercentiles interface{}) *FeeHistoryEstimatorClient_FeeHistory_Call { + return &FeeHistoryEstimatorClient_FeeHistory_Call{Call: _e.mock.On("FeeHistory", ctx, blockCount, rewardPercentiles)} +} + +func (_c *FeeHistoryEstimatorClient_FeeHistory_Call) Run(run func(ctx context.Context, blockCount uint64, rewardPercentiles []float64)) *FeeHistoryEstimatorClient_FeeHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].([]float64)) + }) + return _c +} + +func (_c *FeeHistoryEstimatorClient_FeeHistory_Call) Return(feeHistory *ethereum.FeeHistory, err error) *FeeHistoryEstimatorClient_FeeHistory_Call { + _c.Call.Return(feeHistory, err) + return _c +} + +func (_c *FeeHistoryEstimatorClient_FeeHistory_Call) RunAndReturn(run func(context.Context, uint64, []float64) (*ethereum.FeeHistory, error)) *FeeHistoryEstimatorClient_FeeHistory_Call { + _c.Call.Return(run) + return _c +} + +// SuggestGasPrice provides a mock function with given fields: ctx +func (_m *FeeHistoryEstimatorClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SuggestGasPrice") + } + + var r0 *big.Int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FeeHistoryEstimatorClient_SuggestGasPrice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SuggestGasPrice' +type FeeHistoryEstimatorClient_SuggestGasPrice_Call struct { + *mock.Call +} + +// SuggestGasPrice is a helper method to define mock.On call +// - ctx context.Context +func (_e *FeeHistoryEstimatorClient_Expecter) SuggestGasPrice(ctx interface{}) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + return &FeeHistoryEstimatorClient_SuggestGasPrice_Call{Call: _e.mock.On("SuggestGasPrice", ctx)} +} + +func (_c *FeeHistoryEstimatorClient_SuggestGasPrice_Call) Run(run func(ctx context.Context)) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *FeeHistoryEstimatorClient_SuggestGasPrice_Call) Return(_a0 *big.Int, _a1 error) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FeeHistoryEstimatorClient_SuggestGasPrice_Call) RunAndReturn(run func(context.Context) (*big.Int, error)) *FeeHistoryEstimatorClient_SuggestGasPrice_Call { + _c.Call.Return(run) + return _c +} + +// NewFeeHistoryEstimatorClient creates a new instance of FeeHistoryEstimatorClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFeeHistoryEstimatorClient(t interface { + mock.TestingT + Cleanup(func()) +}) *FeeHistoryEstimatorClient { + mock := &FeeHistoryEstimatorClient{} + 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 c9607091fb..92ae439492 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -26,6 +26,9 @@ import ( evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) +// EstimateGasBuffer is a multiplier applied to estimated gas when the EstimateLimit feature is enabled +const EstimateGasBuffer = float32(1.15) + // EvmFeeEstimator provides a unified interface that wraps EvmEstimator and can determine if legacy or dynamic fee estimation should be used type EvmFeeEstimator interface { services.Service @@ -33,11 +36,11 @@ type EvmFeeEstimator interface { // L1Oracle returns the L1 gas price oracle only if the chain has one, e.g. OP stack L2s and Arbitrum. L1Oracle() rollups.L1Oracle - GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error) + GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (fee EvmFee, estimatedFeeLimit uint64, err error) BumpFee(ctx context.Context, originalFee EvmFee, feeLimit uint64, maxFeePrice *assets.Wei, attempts []EvmPriorAttempt) (bumpedFee EvmFee, chainSpecificFeeLimit uint64, err error) // GetMaxCost returns the total value = max price x fee units + transferred value - GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (*big.Int, error) + GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (*big.Int, error) } type feeEstimatorClient interface { @@ -46,6 +49,9 @@ type feeEstimatorClient interface { CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error ConfiguredChainID() *big.Int HeadByNumber(ctx context.Context, n *big.Int) (*evmtypes.Head, error) + EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) + SuggestGasPrice(ctx context.Context) (*big.Int, error) + FeeHistory(ctx context.Context, blockCount uint64, rewardPercentiles []float64) (feeHistory *ethereum.FeeHistory, err error) } // NewEstimator returns the estimator for a given config @@ -70,6 +76,7 @@ func NewEstimator(lggr logger.Logger, ethClient feeEstimatorClient, cfg Config, "tipCapMin", geCfg.TipCapMin(), "priceMax", geCfg.PriceMax(), "priceMin", geCfg.PriceMin(), + "estimateLimit", geCfg.EstimateLimit(), ) df := geCfg.EIP1559DynamicFees() @@ -105,13 +112,25 @@ func NewEstimator(lggr logger.Logger, ethClient feeEstimatorClient, cfg Config, newEstimator = func(l logger.Logger) EvmEstimator { return NewSuggestedPriceEstimator(lggr, ethClient, geCfg, l1Oracle) } + case "FeeHistory": + newEstimator = func(l logger.Logger) EvmEstimator { + ccfg := FeeHistoryEstimatorConfig{ + BumpPercent: geCfg.BumpPercent(), + CacheTimeout: geCfg.FeeHistory().CacheTimeout(), + EIP1559: geCfg.EIP1559DynamicFees(), + BlockHistorySize: uint64(geCfg.BlockHistory().BlockHistorySize()), + RewardPercentile: float64(geCfg.BlockHistory().TransactionPercentile()), + } + return NewFeeHistoryEstimator(lggr, ethClient, ccfg, ethClient.ConfiguredChainID(), l1Oracle) + } + default: lggr.Warnf("GasEstimator: unrecognised mode '%s', falling back to FixedPriceEstimator", s) newEstimator = func(l logger.Logger) EvmEstimator { return NewFixedPriceEstimator(geCfg, ethClient, bh, lggr, l1Oracle) } } - return NewEvmFeeEstimator(lggr, newEstimator, df, geCfg), nil + return NewEvmFeeEstimator(lggr, newEstimator, df, geCfg, ethClient), nil } // DynamicFee encompasses both FeeCap and TipCap for EIP1559 transactions @@ -182,17 +201,19 @@ type evmFeeEstimator struct { EvmEstimator EIP1559Enabled bool geCfg GasEstimatorConfig + ethClient feeEstimatorClient } var _ EvmFeeEstimator = (*evmFeeEstimator)(nil) -func NewEvmFeeEstimator(lggr logger.Logger, newEstimator func(logger.Logger) EvmEstimator, eip1559Enabled bool, geCfg GasEstimatorConfig) EvmFeeEstimator { +func NewEvmFeeEstimator(lggr logger.Logger, newEstimator func(logger.Logger) EvmEstimator, eip1559Enabled bool, geCfg GasEstimatorConfig, ethClient feeEstimatorClient) EvmFeeEstimator { lggr = logger.Named(lggr, "WrappedEvmEstimator") return &evmFeeEstimator{ lggr: lggr, EvmEstimator: newEstimator(lggr), EIP1559Enabled: eip1559Enabled, geCfg: geCfg, + ethClient: ethClient, } } @@ -262,7 +283,10 @@ func (e *evmFeeEstimator) L1Oracle() rollups.L1Oracle { return e.EvmEstimator.L1Oracle() } -func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee EvmFee, chainSpecificFeeLimit uint64, err error) { +// GetFee returns an initial estimated gas price and gas limit for a transaction +// The gas limit provided by the caller can be adjusted by gas estimation or for 2D fees +func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (fee EvmFee, estimatedFeeLimit uint64, err error) { + var chainSpecificFeeLimit uint64 // get dynamic fee if e.EIP1559Enabled { var dynamicFee DynamicFee @@ -270,24 +294,23 @@ func (e *evmFeeEstimator) GetFee(ctx context.Context, calldata []byte, feeLimit if err != nil { return } - chainSpecificFeeLimit, err = commonfee.ApplyMultiplier(feeLimit, e.geCfg.LimitMultiplier()) fee.DynamicFeeCap = dynamicFee.FeeCap fee.DynamicTipCap = dynamicFee.TipCap - return - } - - // get legacy fee - fee.Legacy, chainSpecificFeeLimit, err = e.EvmEstimator.GetLegacyGas(ctx, calldata, feeLimit, maxFeePrice, opts...) - if err != nil { - return + chainSpecificFeeLimit = feeLimit + } else { + // get legacy fee + fee.Legacy, chainSpecificFeeLimit, err = e.EvmEstimator.GetLegacyGas(ctx, calldata, feeLimit, maxFeePrice, opts...) + if err != nil { + return + } } - chainSpecificFeeLimit, err = commonfee.ApplyMultiplier(chainSpecificFeeLimit, e.geCfg.LimitMultiplier()) + estimatedFeeLimit, err = e.estimateFeeLimit(ctx, chainSpecificFeeLimit, calldata, fromAddress, toAddress) return } -func (e *evmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (*big.Int, error) { - fees, gasLimit, err := e.GetFee(ctx, calldata, feeLimit, maxFeePrice, opts...) +func (e *evmFeeEstimator) GetMaxCost(ctx context.Context, amount assets.Eth, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (*big.Int, error) { + fees, gasLimit, err := e.GetFee(ctx, calldata, feeLimit, maxFeePrice, fromAddress, toAddress, opts...) if err != nil { return nil, err } @@ -338,6 +361,56 @@ func (e *evmFeeEstimator) BumpFee(ctx context.Context, originalFee EvmFee, feeLi return } +func (e *evmFeeEstimator) estimateFeeLimit(ctx context.Context, feeLimit uint64, calldata []byte, fromAddress, toAddress *common.Address) (estimatedFeeLimit uint64, err error) { + // Use the feeLimit * LimitMultiplier as the provided gas limit since this multiplier is applied on top of the caller specified gas limit + providedGasLimit, err := commonfee.ApplyMultiplier(feeLimit, e.geCfg.LimitMultiplier()) + if err != nil { + return estimatedFeeLimit, err + } + // Use provided fee limit by default if EstimateLimit is disabled + if !e.geCfg.EstimateLimit() { + return providedGasLimit, nil + } + // Create call msg for gas limit estimation + // Skip setting Gas to avoid capping the results of the estimation + callMsg := ethereum.CallMsg{ + To: toAddress, + Data: calldata, + } + if fromAddress != nil { + callMsg.From = *fromAddress + } + estimatedGas, estimateErr := e.ethClient.EstimateGas(ctx, callMsg) + if estimateErr != nil { + if providedGasLimit > 0 { + // Do not return error if estimate gas failed, we can still use the provided limit instead since it is an upper limit + e.lggr.Errorw("failed to estimate gas limit. falling back to the provided gas limit with multiplier", "callMsg", callMsg, "providedGasLimitWithMultiplier", providedGasLimit, "error", estimateErr) + return providedGasLimit, nil + } + return estimatedFeeLimit, fmt.Errorf("gas estimation failed and provided gas limit is 0: %w", estimateErr) + } + e.lggr.Debugw("estimated gas", "estimatedGas", estimatedGas, "providedGasLimitWithMultiplier", providedGasLimit) + // Return error if estimated gas without the buffer exceeds the provided gas limit, if provided + // Transaction would be destined to run out of gas and fail + if providedGasLimit > 0 && estimatedGas > providedGasLimit { + e.lggr.Errorw("estimated gas exceeds provided gas limit with multiplier", "estimatedGas", estimatedGas, "providedGasLimitWithMultiplier", providedGasLimit) + return estimatedFeeLimit, commonfee.ErrFeeLimitTooLow + } + // Apply EstimateGasBuffer to the estimated gas limit + estimatedFeeLimit, err = commonfee.ApplyMultiplier(estimatedGas, EstimateGasBuffer) + if err != nil { + return + } + // If provided gas limit is not 0, fallback to it if the buffer causes the estimated gas limit to exceed it + // The provided gas limit should be used as an upper bound to avoid unexpected behavior for products + if providedGasLimit > 0 && estimatedFeeLimit > providedGasLimit { + e.lggr.Debugw("estimated gas limit with buffer exceeds the provided gas limit with multiplier. falling back to the provided gas limit with multiplier", "estimatedGasLimit", estimatedFeeLimit, "providedGasLimitWithMultiplier", providedGasLimit) + estimatedFeeLimit = providedGasLimit + } + + return +} + // Config defines an interface for configuration in the gas package type Config interface { ChainType() chaintype.ChainType @@ -359,6 +432,7 @@ type GasEstimatorConfig interface { PriceMin() *assets.Wei PriceMax() *assets.Wei Mode() string + EstimateLimit() bool } type BlockHistoryConfig interface { diff --git a/core/chains/evm/gas/models_test.go b/core/chains/evm/gas/models_test.go index 92ea901596..f2afc26c85 100644 --- a/core/chains/evm/gas/models_test.go +++ b/core/chains/evm/gas/models_test.go @@ -1,6 +1,7 @@ package gas_test import ( + "errors" "math/big" "testing" @@ -12,12 +13,14 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + commonfee "github.com/smartcontractkit/chainlink/v2/common/fee" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" rollupMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" ) func TestWrappedEvmEstimator(t *testing.T) { @@ -35,9 +38,9 @@ func TestWrappedEvmEstimator(t *testing.T) { est := mocks.NewEvmEstimator(t) est.On("GetDynamicFee", mock.Anything, mock.Anything). - Return(dynamicFee, nil).Twice() + Return(dynamicFee, nil).Times(6) est.On("GetLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(legacyFee, gasLimit, nil).Twice() + Return(legacyFee, gasLimit, nil).Times(6) est.On("BumpDynamicFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(dynamicFee, nil).Once() est.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). @@ -49,6 +52,9 @@ func TestWrappedEvmEstimator(t *testing.T) { mockEstimatorName := "WrappedEvmEstimator" mockEvmEstimatorName := "WrappedEvmEstimator.MockEstimator" + fromAddress := testutils.NewAddress() + toAddress := testutils.NewAddress() + // L1Oracle returns the correct L1Oracle interface t.Run("L1Oracle", func(t *testing.T) { lggr := logger.Test(t) @@ -59,7 +65,7 @@ func TestWrappedEvmEstimator(t *testing.T) { getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } // expect nil - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, nil) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, nil, nil) l1Oracle := estimator.L1Oracle() assert.Nil(t, l1Oracle) @@ -68,7 +74,7 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle, err := rollups.NewL1GasOracle(lggr, nil, chaintype.ChainOptimismBedrock) require.NoError(t, err) // cast oracle to L1Oracle interface - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) evmEstimator.On("L1Oracle").Return(oracle).Once() l1Oracle = estimator.L1Oracle() @@ -80,8 +86,8 @@ func TestWrappedEvmEstimator(t *testing.T) { lggr := logger.Test(t) // expect legacy fee data dynamicFees := false - estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - fee, max, err := estimator.GetFee(ctx, nil, 0, nil) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + fee, max, err := estimator.GetFee(ctx, nil, 0, nil, nil, nil) require.NoError(t, err) assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), max) assert.True(t, legacyFee.Equal(fee.Legacy)) @@ -90,8 +96,8 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect dynamic fee data dynamicFees = true - estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - fee, max, err = estimator.GetFee(ctx, nil, gasLimit, nil) + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + fee, max, err = estimator.GetFee(ctx, nil, gasLimit, nil, nil, nil) require.NoError(t, err) assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), max) assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) @@ -103,7 +109,7 @@ func TestWrappedEvmEstimator(t *testing.T) { t.Run("BumpFee", func(t *testing.T) { lggr := logger.Test(t) dynamicFees := false - estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) // expect legacy fee data fee, max, err := estimator.BumpFee(ctx, gas.EvmFee{Legacy: assets.NewWeiI(0)}, 0, nil, nil) @@ -141,8 +147,8 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect legacy fee data dynamicFees := false - estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - total, err := estimator.GetMaxCost(ctx, val, nil, gasLimit, nil) + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + total, err := estimator.GetMaxCost(ctx, val, nil, gasLimit, nil, nil, nil) require.NoError(t, err) fee := new(big.Int).Mul(legacyFee.ToInt(), big.NewInt(int64(gasLimit))) fee, _ = new(big.Float).Mul(new(big.Float).SetInt(fee), big.NewFloat(float64(limitMultiplier))).Int(nil) @@ -150,8 +156,8 @@ func TestWrappedEvmEstimator(t *testing.T) { // expect dynamic fee data dynamicFees = true - estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg) - total, err = estimator.GetMaxCost(ctx, val, nil, gasLimit, nil) + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, nil) + total, err = estimator.GetMaxCost(ctx, val, nil, gasLimit, nil, nil, nil) require.NoError(t, err) fee = new(big.Int).Mul(dynamicFee.FeeCap.ToInt(), big.NewInt(int64(gasLimit))) fee, _ = new(big.Float).Mul(new(big.Float).SetInt(fee), big.NewFloat(float64(limitMultiplier))).Int(nil) @@ -166,7 +172,7 @@ func TestWrappedEvmEstimator(t *testing.T) { estimator := gas.NewEvmFeeEstimator(lggr, func(logger.Logger) gas.EvmEstimator { return evmEstimator - }, false, geCfg) + }, false, geCfg, nil) require.Equal(t, mockEstimatorName, estimator.Name()) require.Equal(t, mockEvmEstimatorName, evmEstimator.Name()) @@ -185,7 +191,7 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("L1Oracle", mock.Anything).Return(nil).Twice() - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err := estimator.Start(ctx) require.NoError(t, err) err = estimator.Close() @@ -193,7 +199,7 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("L1Oracle", mock.Anything).Return(oracle).Twice() - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err = estimator.Start(ctx) require.NoError(t, err) err = estimator.Close() @@ -210,11 +216,11 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle.On("Ready").Return(nil).Twice() getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err := estimator.Ready() require.NoError(t, err) - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) err = estimator.Ready() require.NoError(t, err) }) @@ -235,7 +241,7 @@ func TestWrappedEvmEstimator(t *testing.T) { oracle.On("HealthReport").Return(map[string]error{oracleKey: oracleError}).Once() getEst := func(logger.Logger) gas.EvmEstimator { return evmEstimator } - estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator := gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) report := estimator.HealthReport() require.True(t, pkgerrors.Is(report[evmEstimatorKey], evmEstimatorError)) require.Nil(t, report[oracleKey]) @@ -243,10 +249,160 @@ func TestWrappedEvmEstimator(t *testing.T) { evmEstimator.On("L1Oracle").Return(oracle).Once() - estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg) + estimator = gas.NewEvmFeeEstimator(lggr, getEst, false, geCfg, nil) report = estimator.HealthReport() require.True(t, pkgerrors.Is(report[evmEstimatorKey], evmEstimatorError)) require.True(t, pkgerrors.Is(report[oracleKey], oracleError)) require.NotNil(t, report[mockEstimatorName]) }) + + t.Run("GetFee, estimate gas limit enabled, succeeds", func(t *testing.T) { + estimatedGasLimit := uint64(5) + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, estimate exceeds provided limit, returns error", func(t *testing.T) { + estimatedGasLimit := uint64(100) + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.ErrorIs(t, err, commonfee.ErrFeeLimitTooLow) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.ErrorIs(t, err, commonfee.ErrFeeLimitTooLow) + }) + + t.Run("GetFee, estimate gas limit enabled, buffer exceeds provided limit, fallsback to provided limit", func(t *testing.T) { + estimatedGasLimit := uint64(15) // same as provided limit + lggr := logger.Test(t) + dynamicFees := false // expect legacy fee data + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + dynamicFees = true // expect dynamic fee data + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, RPC fails and fallsback to provided gas limit", func(t *testing.T) { + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(0), errors.New("something broke")).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, gasLimit, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(gasLimit)*limitMultiplier), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, provided fee limit 0, returns uncapped estimation", func(t *testing.T) { + est.On("GetDynamicFee", mock.Anything, mock.Anything). + Return(dynamicFee, nil).Once() + est.On("GetLegacyGas", mock.Anything, mock.Anything, uint64(0), mock.Anything). + Return(legacyFee, uint64(0), nil).Once() + estimatedGasLimit := uint64(100) // same as provided limit + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err := estimator.GetFee(ctx, []byte{}, uint64(0), nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, legacyFee.Equal(fee.Legacy)) + assert.Nil(t, fee.DynamicTipCap) + assert.Nil(t, fee.DynamicFeeCap) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + fee, limit, err = estimator.GetFee(ctx, []byte{}, 0, nil, &fromAddress, &toAddress) + require.NoError(t, err) + assert.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), limit) + assert.True(t, dynamicFee.FeeCap.Equal(fee.DynamicFeeCap)) + assert.True(t, dynamicFee.TipCap.Equal(fee.DynamicTipCap)) + assert.Nil(t, fee.Legacy) + }) + + t.Run("GetFee, estimate gas limit enabled, provided fee limit 0, returns error on failure", func(t *testing.T) { + est.On("GetDynamicFee", mock.Anything, mock.Anything). + Return(dynamicFee, nil).Once() + est.On("GetLegacyGas", mock.Anything, mock.Anything, uint64(0), mock.Anything). + Return(legacyFee, uint64(0), nil).Once() + lggr := logger.Test(t) + // expect legacy fee data + dynamicFees := false + geCfg.EstimateLimitF = true + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(0), errors.New("something broke")).Twice() + estimator := gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err := estimator.GetFee(ctx, []byte{}, 0, nil, &fromAddress, &toAddress) + require.Error(t, err) + + // expect dynamic fee data + dynamicFees = true + estimator = gas.NewEvmFeeEstimator(lggr, getRootEst, dynamicFees, geCfg, ethClient) + _, _, err = estimator.GetFee(ctx, []byte{}, 0, nil, &fromAddress, &toAddress) + require.Error(t, err) + }) } diff --git a/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go b/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go index c01244db70..d758dc711e 100644 --- a/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go +++ b/core/chains/evm/gas/rollups/arbitrum_l1_oracle.go @@ -112,6 +112,10 @@ func (o *arbitrumL1Oracle) Name() string { return o.logger.Name() } +func (o *arbitrumL1Oracle) ChainType(_ context.Context) chaintype.ChainType { + return o.chainType +} + func (o *arbitrumL1Oracle) Start(ctx context.Context) error { return o.StartOnce(o.Name(), func() error { go o.run() diff --git a/core/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go index f707fab684..4195175598 100644 --- a/core/chains/evm/gas/rollups/l1_oracle.go +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -26,6 +26,7 @@ type L1Oracle interface { GasPrice(ctx context.Context) (*assets.Wei, error) GetGasCost(ctx context.Context, tx *types.Transaction, blockNum *big.Int) (*assets.Wei, error) + ChainType(ctx context.Context) chaintype.ChainType } type l1OracleClient interface { @@ -43,7 +44,7 @@ const ( PollPeriod = 6 * time.Second ) -var supportedChainTypes = []chaintype.ChainType{chaintype.ChainArbitrum, chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainZkSync} +var supportedChainTypes = []chaintype.ChainType{chaintype.ChainArbitrum, chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainZkSync, chaintype.ChainMantle} func IsRollupWithL1Support(chainType chaintype.ChainType) bool { return slices.Contains(supportedChainTypes, chainType) @@ -56,7 +57,7 @@ func NewL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chai var l1Oracle L1Oracle var err error switch chainType { - case chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll: + case chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainMantle: l1Oracle, err = NewOpStackL1GasOracle(lggr, ethClient, chainType) case chaintype.ChainArbitrum: l1Oracle, err = NewArbitrumL1GasOracle(lggr, ethClient) diff --git a/core/chains/evm/gas/rollups/mocks/l1_oracle.go b/core/chains/evm/gas/rollups/mocks/l1_oracle.go index e82cb4ee90..25bb3a2db5 100644 --- a/core/chains/evm/gas/rollups/mocks/l1_oracle.go +++ b/core/chains/evm/gas/rollups/mocks/l1_oracle.go @@ -7,6 +7,8 @@ import ( assets "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + chaintype "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" + context "context" mock "github.com/stretchr/testify/mock" @@ -27,6 +29,52 @@ func (_m *L1Oracle) EXPECT() *L1Oracle_Expecter { return &L1Oracle_Expecter{mock: &_m.Mock} } +// ChainType provides a mock function with given fields: ctx +func (_m *L1Oracle) ChainType(ctx context.Context) chaintype.ChainType { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ChainType") + } + + var r0 chaintype.ChainType + if rf, ok := ret.Get(0).(func(context.Context) chaintype.ChainType); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(chaintype.ChainType) + } + + return r0 +} + +// L1Oracle_ChainType_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChainType' +type L1Oracle_ChainType_Call struct { + *mock.Call +} + +// ChainType is a helper method to define mock.On call +// - ctx context.Context +func (_e *L1Oracle_Expecter) ChainType(ctx interface{}) *L1Oracle_ChainType_Call { + return &L1Oracle_ChainType_Call{Call: _e.mock.On("ChainType", ctx)} +} + +func (_c *L1Oracle_ChainType_Call) Run(run func(ctx context.Context)) *L1Oracle_ChainType_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *L1Oracle_ChainType_Call) Return(_a0 chaintype.ChainType) *L1Oracle_ChainType_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *L1Oracle_ChainType_Call) RunAndReturn(run func(context.Context) chaintype.ChainType) *L1Oracle_ChainType_Call { + _c.Call.Return(run) + return _c +} + // Close provides a mock function with given fields: func (_m *L1Oracle) Close() error { ret := _m.Called() diff --git a/core/chains/evm/gas/rollups/op_l1_oracle.go b/core/chains/evm/gas/rollups/op_l1_oracle.go index 6805cd7095..11babc5ca5 100644 --- a/core/chains/evm/gas/rollups/op_l1_oracle.go +++ b/core/chains/evm/gas/rollups/op_l1_oracle.go @@ -50,6 +50,7 @@ type optimismL1Oracle struct { blobBaseFeeCalldata []byte blobBaseFeeScalarCalldata []byte decimalsCalldata []byte + tokenRatioCalldata []byte isEcotoneCalldata []byte isEcotoneMethodAbi abi.ABI isFjordCalldata []byte @@ -87,7 +88,7 @@ const ( // decimals is a hex encoded call to: // `function decimals() public pure returns (uint256);` decimalsMethod = "decimals" - // OPGasOracleAddress is the address of the precompiled contract that exists on Optimism and Base. + // OPGasOracleAddress is the address of the precompiled contract that exists on Optimism, Base and Mantle. OPGasOracleAddress = "0x420000000000000000000000000000000000000F" // KromaGasOracleAddress is the address of the precompiled contract that exists on Kroma. KromaGasOracleAddress = "0x4200000000000000000000000000000000000005" @@ -98,7 +99,7 @@ const ( func NewOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chaintype.ChainType) (*optimismL1Oracle, error) { var precompileAddress string switch chainType { - case chaintype.ChainOptimismBedrock: + case chaintype.ChainOptimismBedrock, chaintype.ChainMantle: precompileAddress = OPGasOracleAddress case chaintype.ChainKroma: precompileAddress = KromaGasOracleAddress @@ -219,6 +220,10 @@ func (o *optimismL1Oracle) Name() string { return o.logger.Name() } +func (o *optimismL1Oracle) ChainType(_ context.Context) chaintype.ChainType { + return o.chainType +} + func (o *optimismL1Oracle) Start(ctx context.Context) error { return o.StartOnce(o.Name(), func() error { go o.run() diff --git a/core/chains/evm/gas/rollups/zkSync_l1_oracle.go b/core/chains/evm/gas/rollups/zkSync_l1_oracle.go index 31d93bc587..c294123354 100644 --- a/core/chains/evm/gas/rollups/zkSync_l1_oracle.go +++ b/core/chains/evm/gas/rollups/zkSync_l1_oracle.go @@ -83,6 +83,10 @@ func (o *zkSyncL1Oracle) Name() string { return o.logger.Name() } +func (o *zkSyncL1Oracle) ChainType(_ context.Context) chaintype.ChainType { + return o.chainType +} + func (o *zkSyncL1Oracle) Start(ctx context.Context) error { return o.StartOnce(o.Name(), func() error { go o.run() diff --git a/core/chains/evm/txmgr/attempts.go b/core/chains/evm/txmgr/attempts.go index 8566adcb5c..c284ee77bd 100644 --- a/core/chains/evm/txmgr/attempts.go +++ b/core/chains/evm/txmgr/attempts.go @@ -58,7 +58,7 @@ func (c *evmTxAttemptBuilder) NewTxAttempt(ctx context.Context, etx Tx, lggr log // used for L2 re-estimation on broadcasting (note EIP1559 must be disabled otherwise this will fail with mismatched fees + tx type) func (c *evmTxAttemptBuilder) NewTxAttemptWithType(ctx context.Context, etx Tx, lggr logger.Logger, txType int, opts ...feetypes.Opt) (attempt TxAttempt, fee gas.EvmFee, feeLimit uint64, retryable bool, err error) { keySpecificMaxGasPriceWei := c.feeConfig.PriceMaxKey(etx.FromAddress) - fee, feeLimit, err = c.EvmFeeEstimator.GetFee(ctx, etx.EncodedPayload, etx.FeeLimit, keySpecificMaxGasPriceWei, opts...) + fee, feeLimit, err = c.EvmFeeEstimator.GetFee(ctx, etx.EncodedPayload, etx.FeeLimit, keySpecificMaxGasPriceWei, &etx.FromAddress, &etx.ToAddress, opts...) if err != nil { return attempt, fee, feeLimit, true, pkgerrors.Wrap(err, "failed to get fee") // estimator errors are retryable } @@ -71,8 +71,8 @@ func (c *evmTxAttemptBuilder) NewTxAttemptWithType(ctx context.Context, etx Tx, // used in the txm broadcaster + confirmer when tx ix rejected for too low fee or is not included in a timely manner func (c *evmTxAttemptBuilder) NewBumpTxAttempt(ctx context.Context, etx Tx, previousAttempt TxAttempt, priorAttempts []TxAttempt, lggr logger.Logger) (attempt TxAttempt, bumpedFee gas.EvmFee, bumpedFeeLimit uint64, retryable bool, err error) { keySpecificMaxGasPriceWei := c.feeConfig.PriceMaxKey(etx.FromAddress) - - bumpedFee, bumpedFeeLimit, err = c.EvmFeeEstimator.BumpFee(ctx, previousAttempt.TxFee, etx.FeeLimit, keySpecificMaxGasPriceWei, newEvmPriorAttempts(priorAttempts)) + // Use the fee limit from the previous attempt to maintain limits adjusted for 2D fees or by estimation + bumpedFee, bumpedFeeLimit, err = c.EvmFeeEstimator.BumpFee(ctx, previousAttempt.TxFee, previousAttempt.ChainSpecificFeeLimit, keySpecificMaxGasPriceWei, newEvmPriorAttempts(priorAttempts)) if err != nil { return attempt, bumpedFee, bumpedFeeLimit, true, pkgerrors.Wrap(err, "failed to bump fee") // estimator errors are retryable } diff --git a/core/chains/evm/txmgr/attempts_test.go b/core/chains/evm/txmgr/attempts_test.go index 6be8cd7067..5c43368fcc 100644 --- a/core/chains/evm/txmgr/attempts_test.go +++ b/core/chains/evm/txmgr/attempts_test.go @@ -339,7 +339,7 @@ func TestTxm_NewCustomTxAttempt_NonRetryableErrors(t *testing.T) { func TestTxm_EvmTxAttemptBuilder_RetryableEstimatorError(t *testing.T) { est := gasmocks.NewEvmFeeEstimator(t) - est.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{}, uint64(0), pkgerrors.New("fail")) + est.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{}, uint64(0), pkgerrors.New("fail")) est.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{}, uint64(0), pkgerrors.New("fail")) kst := ksmocks.NewEth(t) diff --git a/core/chains/evm/txmgr/broadcaster_test.go b/core/chains/evm/txmgr/broadcaster_test.go index cbbe522ba1..514d533159 100644 --- a/core/chains/evm/txmgr/broadcaster_test.go +++ b/core/chains/evm/txmgr/broadcaster_test.go @@ -29,8 +29,10 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" commonclient "github.com/smartcontractkit/chainlink/v2/common/client" + commmonfee "github.com/smartcontractkit/chainlink/v2/common/fee" txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" @@ -69,7 +71,7 @@ func NewTestEthBroadcaster( estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(config.EVM().GasEstimator(), nil, ge.BlockHistory(), lggr, nil) - }, ge.EIP1559DynamicFees(), ge) + }, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, keyStore, estimator) ethBroadcaster := txmgrcommon.NewBroadcaster(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(config.EVM().GasEstimator()), config.EVM().Transactions(), gconfig.Database().Listener(), keyStore, txBuilder, nonceTracker, lggr, checkerFactory, nonceAutoSync, "") @@ -642,7 +644,7 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_OptimisticLockingOnEthTx(t *testi chStartEstimate := make(chan struct{}) chBlock := make(chan struct{}) - estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, ccfg.EVM().GasEstimator().PriceMaxKey(fromAddress)).Return(gas.EvmFee{Legacy: assets.GWei(32)}, uint64(500), nil).Run(func(_ mock.Arguments) { + estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, ccfg.EVM().GasEstimator().PriceMaxKey(fromAddress), mock.Anything, mock.Anything).Return(gas.EvmFee{Legacy: assets.GWei(32)}, uint64(500), nil).Run(func(_ mock.Arguments) { close(chStartEstimate) <-chBlock }).Once() @@ -1177,7 +1179,7 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_Errors(t *testing.T) { t.Run("callback set by ctor", func(t *testing.T) { estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, evmcfg.EVM().GasEstimator().BlockHistory(), lggr, nil) - }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator()) + }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator(), ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), evmcfg.EVM().GasEstimator(), ethKeyStore, estimator) localNextNonce = getLocalNextNonce(t, nonceTracker, fromAddress) eb2 := txmgr.NewEvmBroadcaster(txStore, txmClient, txmgr.NewEvmTxmConfig(evmcfg.EVM()), txmgr.NewEvmTxmFeeConfig(evmcfg.EVM().GasEstimator()), evmcfg.EVM().Transactions(), cfg.Database().Listener(), ethKeyStore, txBuilder, lggr, &testCheckerFactory{}, false, "") @@ -1666,6 +1668,73 @@ func TestEthBroadcaster_ProcessUnstartedEthTxs_Errors(t *testing.T) { }) } +func TestEthBroadcaster_ProcessUnstartedEthTxs_GasEstimationError(t *testing.T) { + toAddress := testutils.NewAddress() + value := big.Int(assets.NewEthValue(142)) + gasLimit := uint64(242) + encodedPayload := []byte{0, 1} + + db := pgtest.NewSqlxDB(t) + cfg := configtest.NewTestGeneralConfig(t) + cfg.EVMConfigs()[0].GasEstimator.EstimateLimit = ptr(true) // Enabled gas limit estimation + limitMultiplier := float32(1.25) + cfg.EVMConfigs()[0].GasEstimator.LimitMultiplier = ptr(decimal.NewFromFloat32(limitMultiplier)) // Set LimitMultiplier for the buffer + txStore := cltest.NewTestTxStore(t, db) + + ethKeyStore := cltest.NewKeyStore(t, db).Eth() + _, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore) + + config := evmtest.NewChainScopedConfig(t, cfg) + ethClient := testutils.NewEthClientMockWithDefaultChain(t) + ethClient.On("PendingNonceAt", mock.Anything, fromAddress).Return(uint64(0), nil).Once() + lggr := logger.Test(t) + txmClient := txmgr.NewEvmTxmClient(ethClient, nil) + nonceTracker := txmgr.NewNonceTracker(lggr, txStore, txmClient) + ge := config.EVM().GasEstimator() + estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { + return gas.NewFixedPriceEstimator(ge, nil, ge.BlockHistory(), lggr, nil) + }, ge.EIP1559DynamicFees(), ge, ethClient) + txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, estimator) + eb := txmgrcommon.NewBroadcaster(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(config.EVM().GasEstimator()), config.EVM().Transactions(), cfg.Database().Listener(), ethKeyStore, txBuilder, nonceTracker, lggr, &testCheckerFactory{}, false, "") + + // Mark instance as test + eb.XXXTestDisableUnstartedTxAutoProcessing() + servicetest.Run(t, eb) + ctx := tests.Context(t) + t.Run("gas limit lowered after estimation", func(t *testing.T) { + estimatedGasLimit := uint64(100) + etx := mustCreateUnstartedTx(t, txStore, fromAddress, toAddress, encodedPayload, gasLimit, value, testutils.FixtureChainID) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(estimatedGasLimit, nil).Once() + ethClient.On("SendTransactionReturnCode", mock.Anything, mock.MatchedBy(func(tx *gethTypes.Transaction) bool { + return tx.Nonce() == uint64(0) + }), fromAddress).Return(commonclient.Successful, nil).Once() + + // Do the thing + retryable, err := eb.ProcessUnstartedTxs(ctx, fromAddress) + assert.NoError(t, err) + assert.False(t, retryable) + + dbEtx, err := txStore.FindTxWithAttempts(ctx, etx.ID) + require.NoError(t, err) + attempt := dbEtx.TxAttempts[0] + require.Equal(t, uint64(float32(estimatedGasLimit)*gas.EstimateGasBuffer), attempt.ChainSpecificFeeLimit) + }) + t.Run("provided gas limit too low, transaction marked as fatal error", func(t *testing.T) { + etx := mustCreateUnstartedTx(t, txStore, fromAddress, toAddress, encodedPayload, gasLimit, value, testutils.FixtureChainID) + ethClient.On("EstimateGas", mock.Anything, mock.Anything).Return(uint64(float32(gasLimit)*limitMultiplier)+1, nil).Once() + + // Do the thing + retryable, err := eb.ProcessUnstartedTxs(ctx, fromAddress) + assert.NoError(t, err) + assert.False(t, retryable) + + dbEtx, err := txStore.FindTxWithAttempts(ctx, etx.ID) + require.NoError(t, err) + require.Equal(t, txmgrcommon.TxFatalError, dbEtx.State) + require.Equal(t, commmonfee.ErrFeeLimitTooLow.Error(), dbEtx.Error.String) + }) +} + func TestEthBroadcaster_ProcessUnstartedEthTxs_KeystoreErrors(t *testing.T) { toAddress := gethCommon.HexToAddress("0x6C03DDA95a2AEd917EeCc6eddD4b9D16E6380411") value := big.Int(assets.NewEthValue(142)) @@ -1760,15 +1829,15 @@ func TestEthBroadcaster_SyncNonce(t *testing.T) { kst := cltest.NewKeyStore(t, db).Eth() _, fromAddress := cltest.RandomKey{Disabled: false}.MustInsertWithState(t, kst) + ethClient := testutils.NewEthClientMockWithDefaultChain(t) estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, evmcfg.EVM().GasEstimator().BlockHistory(), lggr, nil) - }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator()) + }, evmcfg.EVM().GasEstimator().EIP1559DynamicFees(), evmcfg.EVM().GasEstimator(), ethClient) checkerFactory := &testCheckerFactory{} ge := evmcfg.EVM().GasEstimator() t.Run("does nothing if nonce sync is disabled", func(t *testing.T) { - ethClient := testutils.NewEthClientMockWithDefaultChain(t) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, estimator) kst := ksmocks.NewEth(t) @@ -1838,7 +1907,7 @@ func TestEthBroadcaster_HederaBroadcastValidation(t *testing.T) { ge := evmcfg.EVM().GasEstimator() estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(evmcfg.EVM().GasEstimator(), nil, ge.BlockHistory(), lggr, nil) - }, ge.EIP1559DynamicFees(), ge) + }, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, estimator) checkerFactory := &txmgr.CheckerFactory{Client: ethClient} ctx := tests.Context(t) diff --git a/core/chains/evm/txmgr/confirmer_test.go b/core/chains/evm/txmgr/confirmer_test.go index 45d7ac9e12..82b668f168 100644 --- a/core/chains/evm/txmgr/confirmer_test.go +++ b/core/chains/evm/txmgr/confirmer_test.go @@ -128,7 +128,7 @@ func TestEthConfirmer_Lifecycle(t *testing.T) { newEst := func(logger.Logger) gas.EvmEstimator { return estimator } lggr := logger.Test(t) ge := config.EVM().GasEstimator() - feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ethKeyStore, feeEstimator) stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), config.EVM().Transactions().AutoPurge(), feeEstimator, txStore, ethClient) ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), gconfig.Database(), ethKeyStore, txBuilder, lggr, stuckTxDetector) @@ -1646,7 +1646,7 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing newEst := func(logger.Logger) gas.EvmEstimator { return estimator } estimator.On("BumpLegacyGas", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, uint64(0), pkgerrors.Wrapf(commonfee.ErrConnectivity, "transaction...")) ge := ccfg.EVM().GasEstimator() - feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, feeEstimator) addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", mock.Anything, &cltest.FixtureChainID).Return(addresses, nil).Maybe() @@ -1695,7 +1695,7 @@ func TestEthConfirmer_RebroadcastWhereNecessary_WithConnectivityCheck(t *testing newEst := func(logger.Logger) gas.EvmEstimator { return estimator } // Create confirmer with necessary state ge := ccfg.EVM().GasEstimator() - feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge) + feeEstimator := gas.NewEvmFeeEstimator(lggr, newEst, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, kst, feeEstimator) addresses := []gethCommon.Address{fromAddress} kst.On("EnabledAddressesForChain", mock.Anything, &cltest.FixtureChainID).Return(addresses, nil).Maybe() @@ -3195,7 +3195,7 @@ func TestEthConfirmer_ProcessStuckTransactions(t *testing.T) { fee := gas.EvmFee{Legacy: marketGasPrice} bumpedLegacy := assets.GWei(30) bumpedFee := gas.EvmFee{Legacy: bumpedLegacy} - feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything).Return(fee, uint64(0), nil) + feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything, mock.Anything, mock.Anything).Return(fee, uint64(0), nil) feeEstimator.On("BumpFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(bumpedFee, uint64(10_000), nil) autoPurgeThreshold := uint32(5) autoPurgeMinAttempts := uint32(3) @@ -3286,7 +3286,7 @@ func newEthConfirmer(t testing.TB, txStore txmgr.EvmTxStore, ethClient client.Cl ge := config.EVM().GasEstimator() estimator := gas.NewEvmFeeEstimator(lggr, func(lggr logger.Logger) gas.EvmEstimator { return gas.NewFixedPriceEstimator(ge, nil, ge.BlockHistory(), lggr, nil) - }, ge.EIP1559DynamicFees(), ge) + }, ge.EIP1559DynamicFees(), ge, ethClient) txBuilder := txmgr.NewEvmTxAttemptBuilder(*ethClient.ConfiguredChainID(), ge, ks, estimator) stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, "", assets.NewWei(assets.NewEth(100).ToInt()), config.EVM().Transactions().AutoPurge(), estimator, txStore, ethClient) ec := txmgr.NewEvmConfirmer(txStore, txmgr.NewEvmTxmClient(ethClient, nil), txmgr.NewEvmTxmConfig(config.EVM()), txmgr.NewEvmTxmFeeConfig(ge), config.EVM().Transactions(), gconfig.Database(), ks, txBuilder, lggr, stuckTxDetector) diff --git a/core/chains/evm/txmgr/stuck_tx_detector.go b/core/chains/evm/txmgr/stuck_tx_detector.go index 5d621dc0b2..d756e95058 100644 --- a/core/chains/evm/txmgr/stuck_tx_detector.go +++ b/core/chains/evm/txmgr/stuck_tx_detector.go @@ -25,7 +25,7 @@ import ( ) type stuckTxDetectorGasEstimator interface { - GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, opts ...feetypes.Opt) (fee gas.EvmFee, chainSpecificFeeLimit uint64, err error) + GetFee(ctx context.Context, calldata []byte, feeLimit uint64, maxFeePrice *assets.Wei, fromAddress, toAddress *common.Address, opts ...feetypes.Opt) (fee gas.EvmFee, chainSpecificFeeLimit uint64, err error) } type stuckTxDetectorClient interface { @@ -199,7 +199,7 @@ func (d *stuckTxDetector) detectStuckTransactionsHeuristic(ctx context.Context, defer d.purgeBlockNumLock.RUnlock() // Get gas price from internal gas estimator // Send with max gas price time 2 to prevent the results from being capped. Need the market gas price here. - marketGasPrice, _, err := d.gasEstimator.GetFee(ctx, []byte{}, 0, d.maxPrice.Mul(big.NewInt(2))) + marketGasPrice, _, err := d.gasEstimator.GetFee(ctx, []byte{}, 0, d.maxPrice.Mul(big.NewInt(2)), nil, nil) if err != nil { return txs, fmt.Errorf("failed to get market gas price for overflow detection: %w", err) } @@ -215,6 +215,20 @@ func (d *stuckTxDetector) detectStuckTransactionsHeuristic(ctx context.Context, } // Tx attempts are loaded from newest to oldest oldestBroadcastAttempt, newestBroadcastAttempt, broadcastedAttemptsCount := findBroadcastedAttempts(tx) + d.lggr.Debugf("found %d broadcasted attempts for tx id %d in stuck transaction heuristic", broadcastedAttemptsCount, tx.ID) + + // attempt shouldn't be nil as we validated in FindUnconfirmedTxWithLowestNonce, but added anyway for a "belts and braces" approach + if oldestBroadcastAttempt == nil || newestBroadcastAttempt == nil { + d.lggr.Debugw("failed to find broadcast attempt for tx in stuck transaction heuristic", "tx", tx) + continue + } + + // sanity check + if oldestBroadcastAttempt.BroadcastBeforeBlockNum == nil { + d.lggr.Debugw("BroadcastBeforeBlockNum was not set for broadcast attempt in stuck transaction heuristic", "attempt", oldestBroadcastAttempt) + continue + } + // 2. Check if Threshold amount of blocks have passed since the oldest attempt's broadcast block num if *oldestBroadcastAttempt.BroadcastBeforeBlockNum > blockNum-int64(*d.cfg.Threshold()) { continue @@ -244,17 +258,18 @@ func compareGasFees(attemptGas gas.EvmFee, marketGas gas.EvmFee) int { } // Assumes tx attempts are loaded newest to oldest -func findBroadcastedAttempts(tx Tx) (oldestAttempt TxAttempt, newestAttempt TxAttempt, broadcastedCount uint32) { +func findBroadcastedAttempts(tx Tx) (oldestAttempt *TxAttempt, newestAttempt *TxAttempt, broadcastedCount uint32) { foundNewest := false - for _, attempt := range tx.TxAttempts { + for i := range tx.TxAttempts { + attempt := tx.TxAttempts[i] if attempt.State != types.TxAttemptBroadcast { continue } if !foundNewest { - newestAttempt = attempt + newestAttempt = &attempt foundNewest = true } - oldestAttempt = attempt + oldestAttempt = &attempt broadcastedCount++ } return diff --git a/core/chains/evm/txmgr/stuck_tx_detector_test.go b/core/chains/evm/txmgr/stuck_tx_detector_test.go index def49f8e11..c653d297e8 100644 --- a/core/chains/evm/txmgr/stuck_tx_detector_test.go +++ b/core/chains/evm/txmgr/stuck_tx_detector_test.go @@ -73,7 +73,7 @@ func TestStuckTxDetector_LoadPurgeBlockNumMap(t *testing.T) { feeEstimator := gasmocks.NewEvmFeeEstimator(t) marketGasPrice := assets.GWei(15) fee := gas.EvmFee{Legacy: marketGasPrice} - feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything).Return(fee, uint64(0), nil) + feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything, mock.Anything, mock.Anything).Return(fee, uint64(0), nil) autoPurgeThreshold := uint32(5) autoPurgeMinAttempts := uint32(3) autoPurgeCfg := testAutoPurgeConfig{ @@ -194,7 +194,7 @@ func TestStuckTxDetector_DetectStuckTransactionsHeuristic(t *testing.T) { // Return 10 gwei as market gas price marketGasPrice := tenGwei fee := gas.EvmFee{Legacy: marketGasPrice} - feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything).Return(fee, uint64(0), nil) + feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything, mock.Anything, mock.Anything).Return(fee, uint64(0), nil) ethClient := testutils.NewEthClientMockWithDefaultChain(t) autoPurgeThreshold := uint32(5) autoPurgeMinAttempts := uint32(3) @@ -278,6 +278,15 @@ func TestStuckTxDetector_DetectStuckTransactionsHeuristic(t *testing.T) { require.NoError(t, err) require.Len(t, txs, 1) }) + + t.Run("detects stuck transaction with empty BroadcastBeforeBlockNum in attempts will be skipped without panic", func(t *testing.T) { + _, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore) + enabledAddresses := []common.Address{fromAddress} + mustInsertUnconfirmedTxWithBroadcastAttemptsContainsEmptyBroadcastBeforeBlockNum(t, txStore, 0, fromAddress, autoPurgeMinAttempts, marketGasPrice.Add(oneGwei)) + txs, err := stuckTxDetector.DetectStuckTransactions(ctx, enabledAddresses, blockNum) + require.NoError(t, err) + require.Len(t, txs, 0) + }) } func TestStuckTxDetector_DetectStuckTransactionsZkEVM(t *testing.T) { @@ -435,6 +444,23 @@ func mustInsertUnconfirmedTxWithBroadcastAttempts(t *testing.T, txStore txmgr.Te return etx } +// helper function for edge case where broadcast attempt contains empty pointer +func mustInsertUnconfirmedTxWithBroadcastAttemptsContainsEmptyBroadcastBeforeBlockNum(t *testing.T, txStore txmgr.TestEvmTxStore, nonce int64, fromAddress common.Address, numAttempts uint32, latestGasPrice *assets.Wei) txmgr.Tx { + ctx := tests.Context(t) + etx := cltest.MustInsertUnconfirmedEthTx(t, txStore, nonce, fromAddress) + // Insert attempts from oldest to newest + for i := int64(numAttempts - 1); i >= 0; i-- { + attempt := cltest.NewLegacyEthTxAttempt(t, etx.ID) + attempt.State = txmgrtypes.TxAttemptBroadcast + attempt.BroadcastBeforeBlockNum = nil + attempt.TxFee = gas.EvmFee{Legacy: latestGasPrice.Sub(assets.NewWeiI(i))} + require.NoError(t, txStore.InsertTxAttempt(ctx, &attempt)) + } + etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) + require.NoError(t, err) + return etx +} + func mustInsertFatalErrorTxWithError(t *testing.T, txStore txmgr.TestEvmTxStore, nonce int64, fromAddress common.Address, blockNum int64) txmgr.Tx { etx := cltest.NewEthTx(fromAddress) etx.State = txmgrcommon.TxFatalError diff --git a/core/chains/evm/txmgr/test_helpers.go b/core/chains/evm/txmgr/test_helpers.go index 3b3584a988..b1317cb421 100644 --- a/core/chains/evm/txmgr/test_helpers.go +++ b/core/chains/evm/txmgr/test_helpers.go @@ -73,6 +73,10 @@ func (g *TestGasEstimatorConfig) BlockHistory() evmconfig.BlockHistory { return &TestBlockHistoryConfig{} } +func (g *TestGasEstimatorConfig) FeeHistory() evmconfig.FeeHistory { + return &TestFeeHistoryConfig{} +} + func (g *TestGasEstimatorConfig) EIP1559DynamicFees() bool { return false } func (g *TestGasEstimatorConfig) LimitDefault() uint64 { return 42 } func (g *TestGasEstimatorConfig) BumpPercent() uint16 { return 42 } @@ -89,6 +93,7 @@ func (g *TestGasEstimatorConfig) LimitTransfer() uint64 { return 42 } func (g *TestGasEstimatorConfig) PriceMax() *assets.Wei { return assets.NewWeiI(42) } func (g *TestGasEstimatorConfig) PriceMin() *assets.Wei { return assets.NewWeiI(42) } func (g *TestGasEstimatorConfig) Mode() string { return "FixedPrice" } +func (g *TestGasEstimatorConfig) EstimateLimit() bool { return false } func (g *TestGasEstimatorConfig) LimitJobType() evmconfig.LimitJobType { return &TestLimitJobTypeConfig{} } @@ -120,6 +125,12 @@ func (b *TestBlockHistoryConfig) BlockHistorySize() uint16 { return 42 func (b *TestBlockHistoryConfig) EIP1559FeeCapBufferBlocks() uint16 { return 42 } func (b *TestBlockHistoryConfig) TransactionPercentile() uint16 { return 42 } +type TestFeeHistoryConfig struct { + evmconfig.FeeHistory +} + +func (b *TestFeeHistoryConfig) CacheTimeout() time.Duration { return 0 * time.Second } + type transactionsConfig struct { evmconfig.Transactions e *TestEvmConfig diff --git a/core/chains/legacyevm/chain.go b/core/chains/legacyevm/chain.go index 129c031882..27f2fee43c 100644 --- a/core/chains/legacyevm/chain.go +++ b/core/chains/legacyevm/chain.go @@ -269,6 +269,8 @@ func newChain(ctx context.Context, cfg *evmconfig.ChainScoped, nodes []*toml.Nod var logBroadcaster log.Broadcaster if !opts.AppConfig.EVMRPCEnabled() { logBroadcaster = &log.NullBroadcaster{ErrMsg: fmt.Sprintf("Ethereum is disabled for chain %d", chainID)} + } else if !cfg.EVM().LogBroadcasterEnabled() { + logBroadcaster = &log.NullBroadcaster{ErrMsg: fmt.Sprintf("LogBroadcaster disabled for chain %d", chainID)} } else if opts.GenLogBroadcaster == nil { logORM := log.NewORM(opts.DS, *chainID) logBroadcaster = log.NewBroadcaster(logORM, client, cfg.EVM(), l, highestSeenHead, opts.MailMon) diff --git a/core/config/docs/chains-evm.toml b/core/config/docs/chains-evm.toml index 73ea0ebb35..2f03ed4db2 100644 --- a/core/config/docs/chains-evm.toml +++ b/core/config/docs/chains-evm.toml @@ -98,6 +98,8 @@ RPCBlockQueryDelay = 1 # Default # Block 64 will be treated as finalized by CL Node only when chain's latest finalized block is 65. As chain finalizes blocks in batches of 32, # CL Node has to wait for a whole new batch to be finalized to treat block 64 as finalized. FinalizedBlockOffset = 0 # Default +# LogBroadcasterEnabled is a feature flag for LogBroadcaster, by default it's true. +LogBroadcasterEnabled = true # Default # NoNewFinalizedHeadsThreshold controls how long to wait for new finalized block before `NodePool` marks rpc endpoints as # out-of-sync. Only applicable if `FinalityTagEnabled=true` # @@ -194,6 +196,8 @@ LimitMax = 8_000_000 # Default LimitMultiplier = '1.0' # Default # LimitTransfer is the gas limit used for an ordinary ETH transfer. LimitTransfer = 21_000 # Default +# EstimateLimit enables estimating gas limits for transactions. This feature respects the gas limit provided during transaction creation as an upper bound. +EstimateLimit = false # Default # BumpMin is the minimum fixed amount of wei by which gas is bumped on each transaction attempt. BumpMin = '5 gwei' # Default # BumpPercent is the percentage by which to bump gas on a transaction that has exceeded `BumpThreshold`. The larger of `BumpPercent` and `BumpMin` is taken for gas bumps. @@ -310,6 +314,15 @@ EIP1559FeeCapBufferBlocks = 13 # Example # Setting it lower will tend to set lower gas prices. TransactionPercentile = 60 # Default +[EVM.GasEstimator.FeeHistory] +# CacheTimeout is the time to wait in order to refresh the cached values stored in the FeeHistory estimator. A small jitter is applied so the timeout won't be exactly the same each time. +# +# You want this value to be close to the block time. For slower chains, like Ethereum, you can set it to 12s, the same as the block time. For faster chains you can skip a block or two +# and set it to two times the block time i.e. on Optimism you can set it to 4s. Ideally, you don't want to go lower than 1s since the RTT times of the RPC requests will be comparable to +# the timeout. The estimator is already adding a buffer to account for a potential increase in prices within one or two blocks. On the other hand, slower frequency will fail to refresh +# the prices and end up in stale values. +CacheTimeout = '10s' # Default + # The head tracker continually listens for new heads from the chain. # # In addition to these settings, it log warnings if `EVM.NoNewHeadsThreshold` is exceeded without any new blocks being emitted. @@ -397,6 +410,10 @@ EnforceRepeatableRead = false # Default # trigger declaration of `FinalizedBlockOutOfSync` due to insignificant network delays in broadcasting of the finalized state among RPCs. # RPC will not be picked to handle a request even if this option is set to a nonzero value. DeathDeclarationDelay = '10s' # Default +# NewHeadsPollInterval define an interval for polling new block periodically using http client rather than subscribe to ws feed +# +# Set to 0 to disable. +NewHeadsPollInterval = '0s' # Default # **ADVANCED** # Errors enable the node to provide custom regex patterns to match against error messages from RPCs. [EVM.NodePool.Errors] @@ -450,7 +467,7 @@ ObservationGracePeriod = '1s' # Default [[EVM.Nodes]] # Name is a unique (per-chain) identifier for this node. Name = 'foo' # Example -# WSURL is the WS(S) endpoint for this node. Required for primary nodes. +# WSURL is the WS(S) endpoint for this node. Required for primary nodes when `LogBroadcasterEnabled` is `true` WSURL = 'wss://web.socket/test' # Example # HTTPURL is the HTTP(S) endpoint for this node. Required for all nodes. HTTPURL = 'https://foo.web' # Example diff --git a/core/internal/features/features_test.go b/core/internal/features/features_test.go index 1259597379..7be0307798 100644 --- a/core/internal/features/features_test.go +++ b/core/internal/features/features_test.go @@ -1339,7 +1339,7 @@ func TestIntegration_BlockHistoryEstimator(t *testing.T) { chain := evmtest.MustGetDefaultChain(t, legacyChains) estimator := chain.GasEstimator() - gasPrice, gasLimit, err := estimator.GetFee(testutils.Context(t), nil, 500_000, maxGasPrice) + gasPrice, gasLimit, err := estimator.GetFee(testutils.Context(t), nil, 500_000, maxGasPrice, nil, nil) require.NoError(t, err) assert.Equal(t, uint64(500000), gasLimit) assert.Equal(t, "41.5 gwei", gasPrice.Legacy.String()) @@ -1360,7 +1360,7 @@ func TestIntegration_BlockHistoryEstimator(t *testing.T) { newHeads.TrySend(h43) gomega.NewWithT(t).Eventually(func() string { - gasPrice, _, err := estimator.GetFee(testutils.Context(t), nil, 500000, maxGasPrice) + gasPrice, _, err := estimator.GetFee(testutils.Context(t), nil, 500000, maxGasPrice, nil, nil) require.NoError(t, err) return gasPrice.Legacy.String() }, testutils.WaitTimeout(t), cltest.DBPollingInterval).Should(gomega.Equal("45 gwei")) diff --git a/core/internal/mocks/prometheus_backend.go b/core/internal/mocks/prometheus_backend.go deleted file mode 100644 index d02f7062cb..0000000000 --- a/core/internal/mocks/prometheus_backend.go +++ /dev/null @@ -1,204 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package mocks - -import ( - big "math/big" - - mock "github.com/stretchr/testify/mock" -) - -// PrometheusBackend is an autogenerated mock type for the PrometheusBackend type -type PrometheusBackend struct { - mock.Mock -} - -type PrometheusBackend_Expecter struct { - mock *mock.Mock -} - -func (_m *PrometheusBackend) EXPECT() *PrometheusBackend_Expecter { - return &PrometheusBackend_Expecter{mock: &_m.Mock} -} - -// SetMaxUnconfirmedAge provides a mock function with given fields: _a0, _a1 -func (_m *PrometheusBackend) SetMaxUnconfirmedAge(_a0 *big.Int, _a1 float64) { - _m.Called(_a0, _a1) -} - -// PrometheusBackend_SetMaxUnconfirmedAge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetMaxUnconfirmedAge' -type PrometheusBackend_SetMaxUnconfirmedAge_Call struct { - *mock.Call -} - -// SetMaxUnconfirmedAge is a helper method to define mock.On call -// - _a0 *big.Int -// - _a1 float64 -func (_e *PrometheusBackend_Expecter) SetMaxUnconfirmedAge(_a0 interface{}, _a1 interface{}) *PrometheusBackend_SetMaxUnconfirmedAge_Call { - return &PrometheusBackend_SetMaxUnconfirmedAge_Call{Call: _e.mock.On("SetMaxUnconfirmedAge", _a0, _a1)} -} - -func (_c *PrometheusBackend_SetMaxUnconfirmedAge_Call) Run(run func(_a0 *big.Int, _a1 float64)) *PrometheusBackend_SetMaxUnconfirmedAge_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(*big.Int), args[1].(float64)) - }) - return _c -} - -func (_c *PrometheusBackend_SetMaxUnconfirmedAge_Call) Return() *PrometheusBackend_SetMaxUnconfirmedAge_Call { - _c.Call.Return() - return _c -} - -func (_c *PrometheusBackend_SetMaxUnconfirmedAge_Call) RunAndReturn(run func(*big.Int, float64)) *PrometheusBackend_SetMaxUnconfirmedAge_Call { - _c.Call.Return(run) - return _c -} - -// SetMaxUnconfirmedBlocks provides a mock function with given fields: _a0, _a1 -func (_m *PrometheusBackend) SetMaxUnconfirmedBlocks(_a0 *big.Int, _a1 int64) { - _m.Called(_a0, _a1) -} - -// PrometheusBackend_SetMaxUnconfirmedBlocks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetMaxUnconfirmedBlocks' -type PrometheusBackend_SetMaxUnconfirmedBlocks_Call struct { - *mock.Call -} - -// SetMaxUnconfirmedBlocks is a helper method to define mock.On call -// - _a0 *big.Int -// - _a1 int64 -func (_e *PrometheusBackend_Expecter) SetMaxUnconfirmedBlocks(_a0 interface{}, _a1 interface{}) *PrometheusBackend_SetMaxUnconfirmedBlocks_Call { - return &PrometheusBackend_SetMaxUnconfirmedBlocks_Call{Call: _e.mock.On("SetMaxUnconfirmedBlocks", _a0, _a1)} -} - -func (_c *PrometheusBackend_SetMaxUnconfirmedBlocks_Call) Run(run func(_a0 *big.Int, _a1 int64)) *PrometheusBackend_SetMaxUnconfirmedBlocks_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(*big.Int), args[1].(int64)) - }) - return _c -} - -func (_c *PrometheusBackend_SetMaxUnconfirmedBlocks_Call) Return() *PrometheusBackend_SetMaxUnconfirmedBlocks_Call { - _c.Call.Return() - return _c -} - -func (_c *PrometheusBackend_SetMaxUnconfirmedBlocks_Call) RunAndReturn(run func(*big.Int, int64)) *PrometheusBackend_SetMaxUnconfirmedBlocks_Call { - _c.Call.Return(run) - return _c -} - -// SetPipelineRunsQueued provides a mock function with given fields: n -func (_m *PrometheusBackend) SetPipelineRunsQueued(n int) { - _m.Called(n) -} - -// PrometheusBackend_SetPipelineRunsQueued_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPipelineRunsQueued' -type PrometheusBackend_SetPipelineRunsQueued_Call struct { - *mock.Call -} - -// SetPipelineRunsQueued is a helper method to define mock.On call -// - n int -func (_e *PrometheusBackend_Expecter) SetPipelineRunsQueued(n interface{}) *PrometheusBackend_SetPipelineRunsQueued_Call { - return &PrometheusBackend_SetPipelineRunsQueued_Call{Call: _e.mock.On("SetPipelineRunsQueued", n)} -} - -func (_c *PrometheusBackend_SetPipelineRunsQueued_Call) Run(run func(n int)) *PrometheusBackend_SetPipelineRunsQueued_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(int)) - }) - return _c -} - -func (_c *PrometheusBackend_SetPipelineRunsQueued_Call) Return() *PrometheusBackend_SetPipelineRunsQueued_Call { - _c.Call.Return() - return _c -} - -func (_c *PrometheusBackend_SetPipelineRunsQueued_Call) RunAndReturn(run func(int)) *PrometheusBackend_SetPipelineRunsQueued_Call { - _c.Call.Return(run) - return _c -} - -// SetPipelineTaskRunsQueued provides a mock function with given fields: n -func (_m *PrometheusBackend) SetPipelineTaskRunsQueued(n int) { - _m.Called(n) -} - -// PrometheusBackend_SetPipelineTaskRunsQueued_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPipelineTaskRunsQueued' -type PrometheusBackend_SetPipelineTaskRunsQueued_Call struct { - *mock.Call -} - -// SetPipelineTaskRunsQueued is a helper method to define mock.On call -// - n int -func (_e *PrometheusBackend_Expecter) SetPipelineTaskRunsQueued(n interface{}) *PrometheusBackend_SetPipelineTaskRunsQueued_Call { - return &PrometheusBackend_SetPipelineTaskRunsQueued_Call{Call: _e.mock.On("SetPipelineTaskRunsQueued", n)} -} - -func (_c *PrometheusBackend_SetPipelineTaskRunsQueued_Call) Run(run func(n int)) *PrometheusBackend_SetPipelineTaskRunsQueued_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(int)) - }) - return _c -} - -func (_c *PrometheusBackend_SetPipelineTaskRunsQueued_Call) Return() *PrometheusBackend_SetPipelineTaskRunsQueued_Call { - _c.Call.Return() - return _c -} - -func (_c *PrometheusBackend_SetPipelineTaskRunsQueued_Call) RunAndReturn(run func(int)) *PrometheusBackend_SetPipelineTaskRunsQueued_Call { - _c.Call.Return(run) - return _c -} - -// SetUnconfirmedTransactions provides a mock function with given fields: _a0, _a1 -func (_m *PrometheusBackend) SetUnconfirmedTransactions(_a0 *big.Int, _a1 int64) { - _m.Called(_a0, _a1) -} - -// PrometheusBackend_SetUnconfirmedTransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetUnconfirmedTransactions' -type PrometheusBackend_SetUnconfirmedTransactions_Call struct { - *mock.Call -} - -// SetUnconfirmedTransactions is a helper method to define mock.On call -// - _a0 *big.Int -// - _a1 int64 -func (_e *PrometheusBackend_Expecter) SetUnconfirmedTransactions(_a0 interface{}, _a1 interface{}) *PrometheusBackend_SetUnconfirmedTransactions_Call { - return &PrometheusBackend_SetUnconfirmedTransactions_Call{Call: _e.mock.On("SetUnconfirmedTransactions", _a0, _a1)} -} - -func (_c *PrometheusBackend_SetUnconfirmedTransactions_Call) Run(run func(_a0 *big.Int, _a1 int64)) *PrometheusBackend_SetUnconfirmedTransactions_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(*big.Int), args[1].(int64)) - }) - return _c -} - -func (_c *PrometheusBackend_SetUnconfirmedTransactions_Call) Return() *PrometheusBackend_SetUnconfirmedTransactions_Call { - _c.Call.Return() - return _c -} - -func (_c *PrometheusBackend_SetUnconfirmedTransactions_Call) RunAndReturn(run func(*big.Int, int64)) *PrometheusBackend_SetUnconfirmedTransactions_Call { - _c.Call.Return(run) - return _c -} - -// NewPrometheusBackend creates a new instance of PrometheusBackend. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewPrometheusBackend(t interface { - mock.TestingT - Cleanup(func()) -}) *PrometheusBackend { - mock := &PrometheusBackend{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/core/scripts/ccip/manual-execution/go.mod b/core/scripts/ccip/manual-execution/go.mod index 8d646c03f8..356cf7d8dc 100644 --- a/core/scripts/ccip/manual-execution/go.mod +++ b/core/scripts/ccip/manual-execution/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/ethereum/go-ethereum v1.11.3 github.com/pkg/errors v0.9.1 - github.com/smartcontractkit/chain-selectors v1.0.21 + github.com/smartcontractkit/chain-selectors v1.0.27 go.uber.org/multierr v1.1.0 golang.org/x/crypto v0.1.0 ) diff --git a/core/scripts/ccip/manual-execution/go.sum b/core/scripts/ccip/manual-execution/go.sum index d79872f13f..1e38a8a5e0 100644 --- a/core/scripts/ccip/manual-execution/go.sum +++ b/core/scripts/ccip/manual-execution/go.sum @@ -70,8 +70,8 @@ github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/smartcontractkit/chain-selectors v1.0.21 h1:KCR9SA7PhOexaBzFieHoLv1WonwhVOPtOStpqTmLC4E= -github.com/smartcontractkit/chain-selectors v1.0.21/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= +github.com/smartcontractkit/chain-selectors v1.0.27 h1:VE/ftX9Aae4gnw67yR1raKi+30iWKL/sWq8uyiLHM8k= +github.com/smartcontractkit/chain-selectors v1.0.27/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 13ed1c4aa9..2a8aaff9dd 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -22,7 +22,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.17.0 github.com/shopspring/decimal v1.4.0 - github.com/smartcontractkit/chain-selectors v1.0.21 + github.com/smartcontractkit/chain-selectors v1.0.27 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-common v0.2.1-0.20240717132349-ee5af9b79834 github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000 diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 468c13a670..38c63819f8 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1066,8 +1066,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartcontractkit/chain-selectors v1.0.21 h1:KCR9SA7PhOexaBzFieHoLv1WonwhVOPtOStpqTmLC4E= -github.com/smartcontractkit/chain-selectors v1.0.21/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= +github.com/smartcontractkit/chain-selectors v1.0.27 h1:VE/ftX9Aae4gnw67yR1raKi+30iWKL/sWq8uyiLHM8k= +github.com/smartcontractkit/chain-selectors v1.0.27/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f h1:lQZBOjeYFpCdk0mGQUhbrJipd00tu49xK4zSijC/9Co= diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index de3e4c6280..a759f0ee11 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -49,6 +49,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/feeds" "github.com/smartcontractkit/chainlink/v2/core/services/fluxmonitorv2" "github.com/smartcontractkit/chainlink/v2/core/services/gateway" + "github.com/smartcontractkit/chainlink/v2/core/services/headreporter" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/keeper" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" @@ -60,7 +61,6 @@ import ( externalp2p "github.com/smartcontractkit/chainlink/v2/core/services/p2p/wrapper" "github.com/smartcontractkit/chainlink/v2/core/services/periodicbackup" "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" - "github.com/smartcontractkit/chainlink/v2/core/services/promreporter" "github.com/smartcontractkit/chainlink/v2/core/services/registrysyncer" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/mercury" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/mercury/wsrpc" @@ -324,8 +324,6 @@ func NewApplication(opts ApplicationOpts) (Application, error) { srvcs = append(srvcs, mailMon) srvcs = append(srvcs, relayerChainInterops.Services()...) - promReporter := promreporter.NewPromReporter(opts.DS, legacyEVMChains, globalLogger) - srvcs = append(srvcs, promReporter) // Initialize Local Users ORM and Authentication Provider specified in config // BasicAdminUsersORM is initialized and required regardless of separate Authentication Provider @@ -365,8 +363,16 @@ func NewApplication(opts ApplicationOpts) (Application, error) { workflowORM = workflowstore.NewDBStore(opts.DS, globalLogger, clockwork.NewRealClock()) ) + promReporter := headreporter.NewPrometheusReporter(opts.DS, legacyEVMChains) + chainIDs := make([]*big.Int, legacyEVMChains.Len()) + for i, chain := range legacyEVMChains.Slice() { + chainIDs[i] = chain.ID() + } + telemReporter := headreporter.NewTelemetryReporter(telemetryManager, globalLogger, chainIDs...) + headReporter := headreporter.NewHeadReporterService(opts.DS, globalLogger, promReporter, telemReporter) + srvcs = append(srvcs, headReporter) for _, chain := range legacyEVMChains.Slice() { - chain.HeadBroadcaster().Subscribe(promReporter) + chain.HeadBroadcaster().Subscribe(headReporter) chain.TxManager().RegisterResumeCallback(pipelineRunner.ResumeRun) } diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index e9ed223b9b..ebdfec7117 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -520,6 +520,7 @@ func TestConfig_Marshal(t *testing.T) { LimitMax: ptr[uint64](17), LimitMultiplier: mustDecimal("1.234"), LimitTransfer: ptr[uint64](100), + EstimateLimit: ptr(false), TipCapDefault: assets.NewWeiI(2), TipCapMin: assets.NewWeiI(1), PriceDefault: assets.NewWeiI(math.MaxInt64), @@ -543,6 +544,9 @@ func TestConfig_Marshal(t *testing.T) { EIP1559FeeCapBufferBlocks: ptr[uint16](13), TransactionPercentile: ptr[uint16](15), }, + FeeHistory: evmcfg.FeeHistoryEstimator{ + CacheTimeout: &second, + }, }, KeySpecific: []evmcfg.KeySpecific{ @@ -565,6 +569,7 @@ func TestConfig_Marshal(t *testing.T) { NonceAutoSync: ptr(true), NoNewHeadsThreshold: &minute, OperatorFactoryAddress: mustAddress("0xa5B85635Be42F21f94F28034B7DA440EeFF0F418"), + LogBroadcasterEnabled: ptr(true), RPCDefaultBatchSize: ptr[uint32](17), RPCBlockQueryDelay: ptr[uint16](10), NoNewFinalizedHeadsThreshold: &hour, @@ -599,6 +604,7 @@ func TestConfig_Marshal(t *testing.T) { FinalizedBlockPollInterval: &second, EnforceRepeatableRead: ptr(true), DeathDeclarationDelay: &minute, + NewHeadsPollInterval: &zeroSeconds, Errors: evmcfg.ClientErrors{ NonceTooLow: ptr[string]("(: |^)nonce too low"), NonceTooHigh: ptr[string]("(: |^)nonce too high"), @@ -996,6 +1002,7 @@ MinContractPayment = '9.223372036854775807 link' NonceAutoSync = true NoNewHeadsThreshold = '1m0s' OperatorFactoryAddress = '0xa5B85635Be42F21f94F28034B7DA440EeFF0F418' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 17 RPCBlockQueryDelay = 10 FinalizedBlockOffset = 16 @@ -1024,6 +1031,7 @@ LimitDefault = 12 LimitMax = 17 LimitMultiplier = '1.234' LimitTransfer = 100 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 10 BumpThreshold = 6 @@ -1049,6 +1057,9 @@ CheckInclusionPercentile = 19 EIP1559FeeCapBufferBlocks = 13 TransactionPercentile = 15 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '1s' + [EVM.HeadTracker] HistoryDepth = 15 MaxBufferSize = 17 @@ -1072,6 +1083,7 @@ NodeIsSyncingEnabled = true FinalizedBlockPollInterval = '1s' EnforceRepeatableRead = true DeathDeclarationDelay = '1m0s' +NewHeadsPollInterval = '0s' [EVM.NodePool.Errors] NonceTooLow = '(: |^)nonce too low' @@ -1283,11 +1295,12 @@ func TestConfig_Validate(t *testing.T) { - LDAP.RunUserGroupCN: invalid value (): LDAP ReadUserGroupCN can not be empty - LDAP.RunUserGroupCN: invalid value (): LDAP RunUserGroupCN can not be empty - LDAP.ReadUserGroupCN: invalid value (): LDAP ReadUserGroupCN can not be empty - - EVM: 9 errors: + - EVM: 10 errors: - 1.ChainID: invalid value (1): duplicate - must be unique - 0.Nodes.1.Name: invalid value (foo): duplicate - must be unique - 3.Nodes.4.WSURL: invalid value (ws://dupe.com): duplicate - must be unique - - 0: 3 errors: + - 0: 4 errors: + - Nodes: missing: 0th node (primary) must have a valid WSURL when LogBroadcaster is enabled - GasEstimator.BumpTxDepth: invalid value (11): must be less than or equal to Transactions.MaxInFlight - GasEstimator: 6 errors: - BumpPercent: invalid value (1): may not be less than Geth's default of 10 @@ -1297,14 +1310,12 @@ func TestConfig_Validate(t *testing.T) { - PriceMax: invalid value (10 gwei): must be greater than or equal to PriceDefault - BlockHistory.BlockHistorySize: invalid value (0): must be greater than or equal to 1 with BlockHistory Mode - Nodes: 2 errors: - - 0: 2 errors: - - WSURL: missing: required for primary nodes - - HTTPURL: missing: required for all nodes + - 0.HTTPURL: missing: required for all nodes - 1.HTTPURL: missing: required for all nodes - 1: 10 errors: - ChainType: invalid value (Foo): must not be set with this chain id - Nodes: missing: must have at least one node - - ChainType: invalid value (Foo): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted + - ChainType: invalid value (Foo): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted - HeadTracker.HistoryDepth: invalid value (30): must be greater than or equal to FinalizedBlockOffset - GasEstimator.BumpThreshold: invalid value (0): cannot be 0 if auto-purge feature is enabled for Foo - Transactions.AutoPurge.Threshold: missing: needs to be set if auto-purge feature is enabled for Foo @@ -1317,21 +1328,22 @@ func TestConfig_Validate(t *testing.T) { - 2: 5 errors: - ChainType: invalid value (Arbitrum): only "optimismBedrock" can be used with this chain id - Nodes: missing: must have at least one node - - ChainType: invalid value (Arbitrum): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted + - ChainType: invalid value (Arbitrum): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted - FinalityDepth: invalid value (0): must be greater than or equal to 1 - MinIncomingConfirmations: invalid value (0): must be greater than or equal to 1 - - 3.Nodes: 5 errors: - - 0: 3 errors: + - 3: 3 errors: + - Nodes: missing: 0th node (primary) must have a valid WSURL when LogBroadcaster is enabled + - Nodes: missing: 2th node (primary) must have a valid WSURL when LogBroadcaster is enabled + - Nodes: 5 errors: + - 0: 2 errors: - Name: missing: required for all nodes - - WSURL: missing: required for primary nodes - HTTPURL: empty: required for all nodes - 1: 3 errors: - Name: missing: required for all nodes - WSURL: invalid value (http): must be ws or wss - HTTPURL: missing: required for all nodes - - 2: 3 errors: + - 2: 2 errors: - Name: empty: required for all nodes - - WSURL: missing: required for primary nodes - HTTPURL: invalid value (ws): must be http or https - 3.HTTPURL: missing: required for all nodes - 4.HTTPURL: missing: required for all nodes @@ -1339,6 +1351,7 @@ func TestConfig_Validate(t *testing.T) { - ChainID: missing: required for all chains - Nodes: missing: must have at least one node - 5.Transactions.AutoPurge.DetectionApiUrl: invalid value (): must be set for scroll + - 6.Nodes: missing: 0th node (primary) must have a valid WSURL when http polling is disabled - Cosmos: 5 errors: - 1.ChainID: invalid value (Malaga-420): duplicate - must be unique - 0.Nodes.1.Name: invalid value (test): duplicate - must be unique diff --git a/core/services/chainlink/testdata/config-full.toml b/core/services/chainlink/testdata/config-full.toml index 9df2e70508..1e5643becb 100644 --- a/core/services/chainlink/testdata/config-full.toml +++ b/core/services/chainlink/testdata/config-full.toml @@ -288,6 +288,7 @@ MinContractPayment = '9.223372036854775807 link' NonceAutoSync = true NoNewHeadsThreshold = '1m0s' OperatorFactoryAddress = '0xa5B85635Be42F21f94F28034B7DA440EeFF0F418' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 17 RPCBlockQueryDelay = 10 FinalizedBlockOffset = 16 @@ -316,6 +317,7 @@ LimitDefault = 12 LimitMax = 17 LimitMultiplier = '1.234' LimitTransfer = 100 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 10 BumpThreshold = 6 @@ -341,6 +343,9 @@ CheckInclusionPercentile = 19 EIP1559FeeCapBufferBlocks = 13 TransactionPercentile = 15 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '1s' + [EVM.HeadTracker] HistoryDepth = 15 MaxBufferSize = 17 @@ -364,6 +369,7 @@ NodeIsSyncingEnabled = true FinalizedBlockPollInterval = '1s' EnforceRepeatableRead = true DeathDeclarationDelay = '1m0s' +NewHeadsPollInterval = '0s' [EVM.NodePool.Errors] NonceTooLow = '(: |^)nonce too low' diff --git a/core/services/chainlink/testdata/config-invalid.toml b/core/services/chainlink/testdata/config-invalid.toml index ca22e68c22..411741b1b5 100644 --- a/core/services/chainlink/testdata/config-invalid.toml +++ b/core/services/chainlink/testdata/config-invalid.toml @@ -117,6 +117,25 @@ Name = 'scroll node' WSURL = 'ws://foo.bar' HTTPURl = 'http://foo.bar' +[[EVM]] +ChainID = '100' +LogBroadcasterEnabled = false + +[[EVM.Nodes]] +Name = 'failing-fake' +HTTPURl = 'http://foo.bar1' + +[[EVM]] +ChainID = '101' +LogBroadcasterEnabled = false + +[EVM.NodePool] +NewHeadsPollInterval = '1s' + +[[EVM.Nodes]] +Name = 'passing-fake' +HTTPURl = 'http://foo.bar2' + [[Cosmos]] ChainID = 'Malaga-420' diff --git a/core/services/chainlink/testdata/config-multi-chain-effective.toml b/core/services/chainlink/testdata/config-multi-chain-effective.toml index e560099c2c..efe2cfc1a7 100644 --- a/core/services/chainlink/testdata/config-multi-chain-effective.toml +++ b/core/services/chainlink/testdata/config-multi-chain-effective.toml @@ -275,6 +275,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x3E64Cd889482443324F91bFA9c84fE72A511f48A' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 12 @@ -303,6 +304,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -318,6 +320,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -335,6 +340,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 @@ -375,6 +381,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x8007e24251b1D2Fc518Eb843A701d9cD21fe0aA3' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -403,6 +410,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -418,6 +426,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -435,6 +446,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 @@ -469,6 +481,7 @@ MinIncomingConfirmations = 5 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 100 RPCBlockQueryDelay = 10 FinalizedBlockOffset = 0 @@ -497,6 +510,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -512,6 +526,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -529,6 +546,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/core/services/headreporter/head_reporter.go b/core/services/headreporter/head_reporter.go new file mode 100644 index 0000000000..94de8ae2be --- /dev/null +++ b/core/services/headreporter/head_reporter.go @@ -0,0 +1,111 @@ +package headreporter + +import ( + "context" + "sync" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker/types" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +type ( + HeadReporter interface { + ReportNewHead(ctx context.Context, head *evmtypes.Head) error + ReportPeriodic(ctx context.Context) error + } + + HeadReporterService struct { + services.StateMachine + ds sqlutil.DataSource + lggr logger.Logger + newHeads *mailbox.Mailbox[*evmtypes.Head] + chStop services.StopChan + wgDone sync.WaitGroup + reportPeriod time.Duration + reporters []HeadReporter + unsubscribeFns []func() + } +) + +func NewHeadReporterService(ds sqlutil.DataSource, lggr logger.Logger, reporters ...HeadReporter) *HeadReporterService { + return &HeadReporterService{ + ds: ds, + lggr: lggr.Named("HeadReporter"), + newHeads: mailbox.NewSingle[*evmtypes.Head](), + chStop: make(chan struct{}), + reporters: reporters, + reportPeriod: 15 * time.Second, + } +} + +func (hrd *HeadReporterService) Subscribe(subFn func(types.HeadTrackable) (evmtypes.Head, func())) { + _, unsubscribe := subFn(hrd) + hrd.unsubscribeFns = append(hrd.unsubscribeFns, unsubscribe) +} + +func (hrd *HeadReporterService) Start(context.Context) error { + return hrd.StartOnce(hrd.Name(), func() error { + hrd.wgDone.Add(1) + go hrd.eventLoop() + return nil + }) +} + +func (hrd *HeadReporterService) Close() error { + return hrd.StopOnce(hrd.Name(), func() error { + close(hrd.chStop) + hrd.wgDone.Wait() + return nil + }) +} + +func (hrd *HeadReporterService) Name() string { + return hrd.lggr.Name() +} + +func (hrd *HeadReporterService) HealthReport() map[string]error { + return map[string]error{hrd.Name(): hrd.Healthy()} +} + +func (hrd *HeadReporterService) OnNewLongestChain(ctx context.Context, head *evmtypes.Head) { + hrd.newHeads.Deliver(head) +} + +func (hrd *HeadReporterService) eventLoop() { + hrd.lggr.Debug("Starting event loop") + defer hrd.wgDone.Done() + ctx, cancel := hrd.chStop.NewCtx() + defer cancel() + after := time.After(hrd.reportPeriod) + for { + select { + case <-hrd.newHeads.Notify(): + head, exists := hrd.newHeads.Retrieve() + if !exists { + continue + } + for _, reporter := range hrd.reporters { + err := reporter.ReportNewHead(ctx, head) + if err != nil && ctx.Err() == nil { + hrd.lggr.Errorw("Error reporting new head", "err", err) + } + } + case <-after: + for _, reporter := range hrd.reporters { + err := reporter.ReportPeriodic(ctx) + if err != nil && ctx.Err() == nil { + hrd.lggr.Errorw("Error in periodic report", "err", err) + } + } + after = time.After(hrd.reportPeriod) + case <-hrd.chStop: + return + } + } +} diff --git a/core/services/headreporter/head_reporter_mock.go b/core/services/headreporter/head_reporter_mock.go new file mode 100644 index 0000000000..21978abb86 --- /dev/null +++ b/core/services/headreporter/head_reporter_mock.go @@ -0,0 +1,130 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package headreporter + +import ( + context "context" + + types "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + mock "github.com/stretchr/testify/mock" +) + +// MockHeadReporter is an autogenerated mock type for the HeadReporter type +type MockHeadReporter struct { + mock.Mock +} + +type MockHeadReporter_Expecter struct { + mock *mock.Mock +} + +func (_m *MockHeadReporter) EXPECT() *MockHeadReporter_Expecter { + return &MockHeadReporter_Expecter{mock: &_m.Mock} +} + +// ReportNewHead provides a mock function with given fields: ctx, head +func (_m *MockHeadReporter) ReportNewHead(ctx context.Context, head *types.Head) error { + ret := _m.Called(ctx, head) + + if len(ret) == 0 { + panic("no return value specified for ReportNewHead") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Head) error); ok { + r0 = rf(ctx, head) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockHeadReporter_ReportNewHead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReportNewHead' +type MockHeadReporter_ReportNewHead_Call struct { + *mock.Call +} + +// ReportNewHead is a helper method to define mock.On call +// - ctx context.Context +// - head *types.Head +func (_e *MockHeadReporter_Expecter) ReportNewHead(ctx interface{}, head interface{}) *MockHeadReporter_ReportNewHead_Call { + return &MockHeadReporter_ReportNewHead_Call{Call: _e.mock.On("ReportNewHead", ctx, head)} +} + +func (_c *MockHeadReporter_ReportNewHead_Call) Run(run func(ctx context.Context, head *types.Head)) *MockHeadReporter_ReportNewHead_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.Head)) + }) + return _c +} + +func (_c *MockHeadReporter_ReportNewHead_Call) Return(_a0 error) *MockHeadReporter_ReportNewHead_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockHeadReporter_ReportNewHead_Call) RunAndReturn(run func(context.Context, *types.Head) error) *MockHeadReporter_ReportNewHead_Call { + _c.Call.Return(run) + return _c +} + +// ReportPeriodic provides a mock function with given fields: ctx +func (_m *MockHeadReporter) ReportPeriodic(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ReportPeriodic") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockHeadReporter_ReportPeriodic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReportPeriodic' +type MockHeadReporter_ReportPeriodic_Call struct { + *mock.Call +} + +// ReportPeriodic is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockHeadReporter_Expecter) ReportPeriodic(ctx interface{}) *MockHeadReporter_ReportPeriodic_Call { + return &MockHeadReporter_ReportPeriodic_Call{Call: _e.mock.On("ReportPeriodic", ctx)} +} + +func (_c *MockHeadReporter_ReportPeriodic_Call) Run(run func(ctx context.Context)) *MockHeadReporter_ReportPeriodic_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockHeadReporter_ReportPeriodic_Call) Return(_a0 error) *MockHeadReporter_ReportPeriodic_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockHeadReporter_ReportPeriodic_Call) RunAndReturn(run func(context.Context) error) *MockHeadReporter_ReportPeriodic_Call { + _c.Call.Return(run) + return _c +} + +// NewMockHeadReporter creates a new instance of MockHeadReporter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockHeadReporter(t interface { + mock.TestingT + Cleanup(func()) +}) *MockHeadReporter { + mock := &MockHeadReporter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/services/headreporter/head_reporter_test.go b/core/services/headreporter/head_reporter_test.go new file mode 100644 index 0000000000..304dd59a47 --- /dev/null +++ b/core/services/headreporter/head_reporter_test.go @@ -0,0 +1,51 @@ +package headreporter + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" + + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +func NewHead() evmtypes.Head { + return evmtypes.Head{Number: 42, EVMChainID: ubig.NewI(0)} +} + +func Test_HeadReporterService(t *testing.T) { + t.Run("report everything", func(t *testing.T) { + db := pgtest.NewSqlxDB(t) + + headReporter := NewMockHeadReporter(t) + service := NewHeadReporterService(db, logger.TestLogger(t), headReporter) + service.reportPeriod = time.Second + err := service.Start(testutils.Context(t)) + require.NoError(t, err) + + var reportCalls atomic.Int32 + head := NewHead() + headReporter.On("ReportNewHead", mock.Anything, &head).Run(func(args mock.Arguments) { + reportCalls.Add(1) + }).Return(nil) + headReporter.On("ReportPeriodic", mock.Anything).Run(func(args mock.Arguments) { + reportCalls.Add(1) + }).Return(nil) + service.OnNewLongestChain(testutils.Context(t), &head) + + require.Eventually(t, func() bool { return reportCalls.Load() == 2 }, 5*time.Second, 100*time.Millisecond) + }) + + t.Run("has default report period", func(t *testing.T) { + service := NewHeadReporterService(pgtest.NewSqlxDB(t), logger.TestLogger(t), NewMockHeadReporter(t)) + assert.Equal(t, service.reportPeriod, 15*time.Second) + }) +} diff --git a/core/services/headreporter/helper_test.go b/core/services/headreporter/helper_test.go new file mode 100644 index 0000000000..fa05182a85 --- /dev/null +++ b/core/services/headreporter/helper_test.go @@ -0,0 +1,5 @@ +package headreporter + +func (p *prometheusReporter) SetBackend(b PrometheusBackend) { + p.backend = b +} diff --git a/core/services/headreporter/prometheus_backend_mock.go b/core/services/headreporter/prometheus_backend_mock.go new file mode 100644 index 0000000000..ca83f6c4fb --- /dev/null +++ b/core/services/headreporter/prometheus_backend_mock.go @@ -0,0 +1,204 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package headreporter + +import ( + big "math/big" + + mock "github.com/stretchr/testify/mock" +) + +// MockPrometheusBackend is an autogenerated mock type for the PrometheusBackend type +type MockPrometheusBackend struct { + mock.Mock +} + +type MockPrometheusBackend_Expecter struct { + mock *mock.Mock +} + +func (_m *MockPrometheusBackend) EXPECT() *MockPrometheusBackend_Expecter { + return &MockPrometheusBackend_Expecter{mock: &_m.Mock} +} + +// SetMaxUnconfirmedAge provides a mock function with given fields: _a0, _a1 +func (_m *MockPrometheusBackend) SetMaxUnconfirmedAge(_a0 *big.Int, _a1 float64) { + _m.Called(_a0, _a1) +} + +// MockPrometheusBackend_SetMaxUnconfirmedAge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetMaxUnconfirmedAge' +type MockPrometheusBackend_SetMaxUnconfirmedAge_Call struct { + *mock.Call +} + +// SetMaxUnconfirmedAge is a helper method to define mock.On call +// - _a0 *big.Int +// - _a1 float64 +func (_e *MockPrometheusBackend_Expecter) SetMaxUnconfirmedAge(_a0 interface{}, _a1 interface{}) *MockPrometheusBackend_SetMaxUnconfirmedAge_Call { + return &MockPrometheusBackend_SetMaxUnconfirmedAge_Call{Call: _e.mock.On("SetMaxUnconfirmedAge", _a0, _a1)} +} + +func (_c *MockPrometheusBackend_SetMaxUnconfirmedAge_Call) Run(run func(_a0 *big.Int, _a1 float64)) *MockPrometheusBackend_SetMaxUnconfirmedAge_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*big.Int), args[1].(float64)) + }) + return _c +} + +func (_c *MockPrometheusBackend_SetMaxUnconfirmedAge_Call) Return() *MockPrometheusBackend_SetMaxUnconfirmedAge_Call { + _c.Call.Return() + return _c +} + +func (_c *MockPrometheusBackend_SetMaxUnconfirmedAge_Call) RunAndReturn(run func(*big.Int, float64)) *MockPrometheusBackend_SetMaxUnconfirmedAge_Call { + _c.Call.Return(run) + return _c +} + +// SetMaxUnconfirmedBlocks provides a mock function with given fields: _a0, _a1 +func (_m *MockPrometheusBackend) SetMaxUnconfirmedBlocks(_a0 *big.Int, _a1 int64) { + _m.Called(_a0, _a1) +} + +// MockPrometheusBackend_SetMaxUnconfirmedBlocks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetMaxUnconfirmedBlocks' +type MockPrometheusBackend_SetMaxUnconfirmedBlocks_Call struct { + *mock.Call +} + +// SetMaxUnconfirmedBlocks is a helper method to define mock.On call +// - _a0 *big.Int +// - _a1 int64 +func (_e *MockPrometheusBackend_Expecter) SetMaxUnconfirmedBlocks(_a0 interface{}, _a1 interface{}) *MockPrometheusBackend_SetMaxUnconfirmedBlocks_Call { + return &MockPrometheusBackend_SetMaxUnconfirmedBlocks_Call{Call: _e.mock.On("SetMaxUnconfirmedBlocks", _a0, _a1)} +} + +func (_c *MockPrometheusBackend_SetMaxUnconfirmedBlocks_Call) Run(run func(_a0 *big.Int, _a1 int64)) *MockPrometheusBackend_SetMaxUnconfirmedBlocks_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*big.Int), args[1].(int64)) + }) + return _c +} + +func (_c *MockPrometheusBackend_SetMaxUnconfirmedBlocks_Call) Return() *MockPrometheusBackend_SetMaxUnconfirmedBlocks_Call { + _c.Call.Return() + return _c +} + +func (_c *MockPrometheusBackend_SetMaxUnconfirmedBlocks_Call) RunAndReturn(run func(*big.Int, int64)) *MockPrometheusBackend_SetMaxUnconfirmedBlocks_Call { + _c.Call.Return(run) + return _c +} + +// SetPipelineRunsQueued provides a mock function with given fields: n +func (_m *MockPrometheusBackend) SetPipelineRunsQueued(n int) { + _m.Called(n) +} + +// MockPrometheusBackend_SetPipelineRunsQueued_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPipelineRunsQueued' +type MockPrometheusBackend_SetPipelineRunsQueued_Call struct { + *mock.Call +} + +// SetPipelineRunsQueued is a helper method to define mock.On call +// - n int +func (_e *MockPrometheusBackend_Expecter) SetPipelineRunsQueued(n interface{}) *MockPrometheusBackend_SetPipelineRunsQueued_Call { + return &MockPrometheusBackend_SetPipelineRunsQueued_Call{Call: _e.mock.On("SetPipelineRunsQueued", n)} +} + +func (_c *MockPrometheusBackend_SetPipelineRunsQueued_Call) Run(run func(n int)) *MockPrometheusBackend_SetPipelineRunsQueued_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int)) + }) + return _c +} + +func (_c *MockPrometheusBackend_SetPipelineRunsQueued_Call) Return() *MockPrometheusBackend_SetPipelineRunsQueued_Call { + _c.Call.Return() + return _c +} + +func (_c *MockPrometheusBackend_SetPipelineRunsQueued_Call) RunAndReturn(run func(int)) *MockPrometheusBackend_SetPipelineRunsQueued_Call { + _c.Call.Return(run) + return _c +} + +// SetPipelineTaskRunsQueued provides a mock function with given fields: n +func (_m *MockPrometheusBackend) SetPipelineTaskRunsQueued(n int) { + _m.Called(n) +} + +// MockPrometheusBackend_SetPipelineTaskRunsQueued_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPipelineTaskRunsQueued' +type MockPrometheusBackend_SetPipelineTaskRunsQueued_Call struct { + *mock.Call +} + +// SetPipelineTaskRunsQueued is a helper method to define mock.On call +// - n int +func (_e *MockPrometheusBackend_Expecter) SetPipelineTaskRunsQueued(n interface{}) *MockPrometheusBackend_SetPipelineTaskRunsQueued_Call { + return &MockPrometheusBackend_SetPipelineTaskRunsQueued_Call{Call: _e.mock.On("SetPipelineTaskRunsQueued", n)} +} + +func (_c *MockPrometheusBackend_SetPipelineTaskRunsQueued_Call) Run(run func(n int)) *MockPrometheusBackend_SetPipelineTaskRunsQueued_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int)) + }) + return _c +} + +func (_c *MockPrometheusBackend_SetPipelineTaskRunsQueued_Call) Return() *MockPrometheusBackend_SetPipelineTaskRunsQueued_Call { + _c.Call.Return() + return _c +} + +func (_c *MockPrometheusBackend_SetPipelineTaskRunsQueued_Call) RunAndReturn(run func(int)) *MockPrometheusBackend_SetPipelineTaskRunsQueued_Call { + _c.Call.Return(run) + return _c +} + +// SetUnconfirmedTransactions provides a mock function with given fields: _a0, _a1 +func (_m *MockPrometheusBackend) SetUnconfirmedTransactions(_a0 *big.Int, _a1 int64) { + _m.Called(_a0, _a1) +} + +// MockPrometheusBackend_SetUnconfirmedTransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetUnconfirmedTransactions' +type MockPrometheusBackend_SetUnconfirmedTransactions_Call struct { + *mock.Call +} + +// SetUnconfirmedTransactions is a helper method to define mock.On call +// - _a0 *big.Int +// - _a1 int64 +func (_e *MockPrometheusBackend_Expecter) SetUnconfirmedTransactions(_a0 interface{}, _a1 interface{}) *MockPrometheusBackend_SetUnconfirmedTransactions_Call { + return &MockPrometheusBackend_SetUnconfirmedTransactions_Call{Call: _e.mock.On("SetUnconfirmedTransactions", _a0, _a1)} +} + +func (_c *MockPrometheusBackend_SetUnconfirmedTransactions_Call) Run(run func(_a0 *big.Int, _a1 int64)) *MockPrometheusBackend_SetUnconfirmedTransactions_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*big.Int), args[1].(int64)) + }) + return _c +} + +func (_c *MockPrometheusBackend_SetUnconfirmedTransactions_Call) Return() *MockPrometheusBackend_SetUnconfirmedTransactions_Call { + _c.Call.Return() + return _c +} + +func (_c *MockPrometheusBackend_SetUnconfirmedTransactions_Call) RunAndReturn(run func(*big.Int, int64)) *MockPrometheusBackend_SetUnconfirmedTransactions_Call { + _c.Call.Return(run) + return _c +} + +// NewMockPrometheusBackend creates a new instance of MockPrometheusBackend. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockPrometheusBackend(t interface { + mock.TestingT + Cleanup(func()) +}) *MockPrometheusBackend { + mock := &MockPrometheusBackend{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/services/promreporter/prom_reporter.go b/core/services/headreporter/prometheus_reporter.go similarity index 63% rename from core/services/promreporter/prom_reporter.go rename to core/services/headreporter/prometheus_reporter.go index 31d5f1129e..3e39c7aca4 100644 --- a/core/services/promreporter/prom_reporter.go +++ b/core/services/headreporter/prometheus_reporter.go @@ -1,40 +1,28 @@ -package promreporter +package headreporter import ( "context" "fmt" "math/big" - "sync" "time" - "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" - txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" - "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "go.uber.org/multierr" - "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" - + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" - "github.com/smartcontractkit/chainlink/v2/core/logger" ) type ( - promReporter struct { - services.StateMachine - ds sqlutil.DataSource - chains legacyevm.LegacyChainContainer - lggr logger.Logger - backend PrometheusBackend - newHeads *mailbox.Mailbox[*evmtypes.Head] - chStop services.StopChan - wgDone sync.WaitGroup - reportPeriod time.Duration + prometheusReporter struct { + ds sqlutil.DataSource + chains legacyevm.LegacyChainContainer + backend PrometheusBackend } PrometheusBackend interface { @@ -71,103 +59,15 @@ var ( }) ) -func (defaultBackend) SetUnconfirmedTransactions(evmChainID *big.Int, n int64) { - promUnconfirmedTransactions.WithLabelValues(evmChainID.String()).Set(float64(n)) -} - -func (defaultBackend) SetMaxUnconfirmedAge(evmChainID *big.Int, s float64) { - promMaxUnconfirmedAge.WithLabelValues(evmChainID.String()).Set(s) -} - -func (defaultBackend) SetMaxUnconfirmedBlocks(evmChainID *big.Int, n int64) { - promMaxUnconfirmedBlocks.WithLabelValues(evmChainID.String()).Set(float64(n)) -} - -func (defaultBackend) SetPipelineRunsQueued(n int) { - promPipelineTaskRunsQueued.Set(float64(n)) -} - -func (defaultBackend) SetPipelineTaskRunsQueued(n int) { - promPipelineRunsQueued.Set(float64(n)) -} - -func NewPromReporter(ds sqlutil.DataSource, chainContainer legacyevm.LegacyChainContainer, lggr logger.Logger, opts ...interface{}) *promReporter { - var backend PrometheusBackend = defaultBackend{} - period := 15 * time.Second - for _, opt := range opts { - switch v := opt.(type) { - case time.Duration: - period = v - case PrometheusBackend: - backend = v - } - } - - chStop := make(chan struct{}) - return &promReporter{ - ds: ds, - chains: chainContainer, - lggr: lggr.Named("PromReporter"), - backend: backend, - newHeads: mailbox.NewSingle[*evmtypes.Head](), - chStop: chStop, - reportPeriod: period, +func NewPrometheusReporter(ds sqlutil.DataSource, chainContainer legacyevm.LegacyChainContainer) *prometheusReporter { + return &prometheusReporter{ + ds: ds, + chains: chainContainer, + backend: defaultBackend{}, } } -// Start starts PromReporter. -func (pr *promReporter) Start(context.Context) error { - return pr.StartOnce("PromReporter", func() error { - pr.wgDone.Add(1) - go pr.eventLoop() - return nil - }) -} - -func (pr *promReporter) Close() error { - return pr.StopOnce("PromReporter", func() error { - close(pr.chStop) - pr.wgDone.Wait() - return nil - }) -} -func (pr *promReporter) Name() string { - return pr.lggr.Name() -} - -func (pr *promReporter) HealthReport() map[string]error { - return map[string]error{pr.Name(): pr.Healthy()} -} - -func (pr *promReporter) OnNewLongestChain(ctx context.Context, head *evmtypes.Head) { - pr.newHeads.Deliver(head) -} - -func (pr *promReporter) eventLoop() { - pr.lggr.Debug("Starting event loop") - defer pr.wgDone.Done() - ctx, cancel := pr.chStop.NewCtx() - defer cancel() - for { - select { - case <-pr.newHeads.Notify(): - head, exists := pr.newHeads.Retrieve() - if !exists { - continue - } - pr.reportHeadMetrics(ctx, head) - case <-time.After(pr.reportPeriod): - if err := errors.Wrap(pr.reportPipelineRunStats(ctx), "reportPipelineRunStats failed"); err != nil { - pr.lggr.Errorw("Error reporting prometheus metrics", "err", err) - } - - case <-pr.chStop: - return - } - } -} - -func (pr *promReporter) getTxm(evmChainID *big.Int) (txmgr.TxManager, error) { +func (pr *prometheusReporter) getTxm(evmChainID *big.Int) (txmgr.TxManager, error) { chain, err := pr.chains.Get(evmChainID.String()) if err != nil { return nil, fmt.Errorf("failed to get chain: %w", err) @@ -175,20 +75,16 @@ func (pr *promReporter) getTxm(evmChainID *big.Int) (txmgr.TxManager, error) { return chain.TxManager(), nil } -func (pr *promReporter) reportHeadMetrics(ctx context.Context, head *evmtypes.Head) { +func (pr *prometheusReporter) ReportNewHead(ctx context.Context, head *evmtypes.Head) error { evmChainID := head.EVMChainID.ToInt() - err := multierr.Combine( + return multierr.Combine( errors.Wrap(pr.reportPendingEthTxes(ctx, evmChainID), "reportPendingEthTxes failed"), errors.Wrap(pr.reportMaxUnconfirmedAge(ctx, evmChainID), "reportMaxUnconfirmedAge failed"), errors.Wrap(pr.reportMaxUnconfirmedBlocks(ctx, head), "reportMaxUnconfirmedBlocks failed"), ) - - if err != nil && ctx.Err() == nil { - pr.lggr.Errorw("Error reporting prometheus metrics", "err", err) - } } -func (pr *promReporter) reportPendingEthTxes(ctx context.Context, evmChainID *big.Int) (err error) { +func (pr *prometheusReporter) reportPendingEthTxes(ctx context.Context, evmChainID *big.Int) (err error) { txm, err := pr.getTxm(evmChainID) if err != nil { return fmt.Errorf("failed to get txm: %w", err) @@ -202,7 +98,7 @@ func (pr *promReporter) reportPendingEthTxes(ctx context.Context, evmChainID *bi return nil } -func (pr *promReporter) reportMaxUnconfirmedAge(ctx context.Context, evmChainID *big.Int) (err error) { +func (pr *prometheusReporter) reportMaxUnconfirmedAge(ctx context.Context, evmChainID *big.Int) (err error) { txm, err := pr.getTxm(evmChainID) if err != nil { return fmt.Errorf("failed to get txm: %w", err) @@ -221,7 +117,7 @@ func (pr *promReporter) reportMaxUnconfirmedAge(ctx context.Context, evmChainID return nil } -func (pr *promReporter) reportMaxUnconfirmedBlocks(ctx context.Context, head *evmtypes.Head) (err error) { +func (pr *prometheusReporter) reportMaxUnconfirmedBlocks(ctx context.Context, head *evmtypes.Head) (err error) { txm, err := pr.getTxm(head.EVMChainID.ToInt()) if err != nil { return fmt.Errorf("failed to get txm: %w", err) @@ -240,7 +136,11 @@ func (pr *promReporter) reportMaxUnconfirmedBlocks(ctx context.Context, head *ev return nil } -func (pr *promReporter) reportPipelineRunStats(ctx context.Context) (err error) { +func (pr *prometheusReporter) ReportPeriodic(ctx context.Context) error { + return errors.Wrap(pr.reportPipelineRunStats(ctx), "reportPipelineRunStats failed") +} + +func (pr *prometheusReporter) reportPipelineRunStats(ctx context.Context) (err error) { rows, err := pr.ds.QueryContext(ctx, ` SELECT pipeline_run_id FROM pipeline_task_runs WHERE finished_at IS NULL `) @@ -271,3 +171,23 @@ SELECT pipeline_run_id FROM pipeline_task_runs WHERE finished_at IS NULL return nil } + +func (defaultBackend) SetUnconfirmedTransactions(evmChainID *big.Int, n int64) { + promUnconfirmedTransactions.WithLabelValues(evmChainID.String()).Set(float64(n)) +} + +func (defaultBackend) SetMaxUnconfirmedAge(evmChainID *big.Int, s float64) { + promMaxUnconfirmedAge.WithLabelValues(evmChainID.String()).Set(s) +} + +func (defaultBackend) SetMaxUnconfirmedBlocks(evmChainID *big.Int, n int64) { + promMaxUnconfirmedBlocks.WithLabelValues(evmChainID.String()).Set(float64(n)) +} + +func (defaultBackend) SetPipelineRunsQueued(n int) { + promPipelineTaskRunsQueued.Set(float64(n)) +} + +func (defaultBackend) SetPipelineTaskRunsQueued(n int) { + promPipelineRunsQueued.Set(float64(n)) +} diff --git a/core/services/promreporter/prom_reporter_test.go b/core/services/headreporter/prometheus_reporter_test.go similarity index 64% rename from core/services/promreporter/prom_reporter_test.go rename to core/services/headreporter/prometheus_reporter_test.go index b61fa25bdc..32d2c09d0e 100644 --- a/core/services/promreporter/prom_reporter_test.go +++ b/core/services/headreporter/prometheus_reporter_test.go @@ -1,8 +1,7 @@ -package promreporter_test +package headreporter_test import ( "math/big" - "sync/atomic" "testing" "time" @@ -10,90 +9,40 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" - "github.com/smartcontractkit/chainlink/v2/core/internal/mocks" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/services/promreporter" + "github.com/smartcontractkit/chainlink/v2/core/services/headreporter" ) -func newHead() evmtypes.Head { - return evmtypes.Head{Number: 42, EVMChainID: ubig.NewI(0)} -} - -func newLegacyChainContainer(t *testing.T, db *sqlx.DB) legacyevm.LegacyChainContainer { - config, dbConfig, evmConfig := txmgr.MakeTestConfigs(t) - keyStore := cltest.NewKeyStore(t, db).Eth() - ethClient := evmtest.NewEthClientMockWithDefaultChain(t) - estimator, err := gas.NewEstimator(logger.TestLogger(t), ethClient, config, evmConfig.GasEstimator()) - require.NoError(t, err) - lggr := logger.TestLogger(t) - lpOpts := logpoller.Opts{ - PollPeriod: 100 * time.Millisecond, - FinalityDepth: 2, - BackfillBatchSize: 3, - RpcBatchSize: 2, - KeepFinalizedBlocksDepth: 1000, - } - ht := headtracker.NewSimulatedHeadTracker(ethClient, lpOpts.UseFinalityTag, lpOpts.FinalityDepth) - lp := logpoller.NewLogPoller(logpoller.NewORM(testutils.FixtureChainID, db, lggr), ethClient, lggr, ht, lpOpts) - - txm, err := txmgr.NewTxm( - db, - evmConfig, - evmConfig.GasEstimator(), - evmConfig.Transactions(), - nil, - dbConfig, - dbConfig.Listener(), - ethClient, - lggr, - lp, - keyStore, - estimator) - require.NoError(t, err) - - cfg := configtest.NewGeneralConfig(t, nil) - return cltest.NewLegacyChainsWithMockChainAndTxManager(t, ethClient, cfg, txm) -} - -func Test_PromReporter_OnNewLongestChain(t *testing.T) { +func Test_PrometheusReporter(t *testing.T) { t.Run("with nothing in the database", func(t *testing.T) { db := pgtest.NewSqlxDB(t) - backend := mocks.NewPrometheusBackend(t) - reporter := promreporter.NewPromReporter(db, newLegacyChainContainer(t, db), logger.TestLogger(t), backend, 10*time.Millisecond) - - var subscribeCalls atomic.Int32 - + backend := headreporter.NewMockPrometheusBackend(t) backend.On("SetUnconfirmedTransactions", big.NewInt(0), int64(0)).Return() backend.On("SetMaxUnconfirmedAge", big.NewInt(0), float64(0)).Return() backend.On("SetMaxUnconfirmedBlocks", big.NewInt(0), int64(0)).Return() - backend.On("SetPipelineTaskRunsQueued", 0).Return() - backend.On("SetPipelineRunsQueued", 0). - Run(func(args mock.Arguments) { - subscribeCalls.Add(1) - }). - Return() - servicetest.Run(t, reporter) + reporter := headreporter.NewPrometheusReporter(db, newLegacyChainContainer(t, db)) + reporter.SetBackend(backend) - head := newHead() - reporter.OnNewLongestChain(testutils.Context(t), &head) + head := headreporter.NewHead() + err := reporter.ReportNewHead(testutils.Context(t), &head) + require.NoError(t, err) - require.Eventually(t, func() bool { return subscribeCalls.Load() >= 1 }, 12*time.Second, 100*time.Millisecond) + backend.On("SetPipelineTaskRunsQueued", 0).Return() + backend.On("SetPipelineRunsQueued", 0).Return() + err = reporter.ReportPeriodic(testutils.Context(t)) + require.NoError(t, err) }) t.Run("with unconfirmed evm.txes", func(t *testing.T) { @@ -102,61 +51,92 @@ func Test_PromReporter_OnNewLongestChain(t *testing.T) { ethKeyStore := cltest.NewKeyStore(t, db).Eth() _, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore) - var subscribeCalls atomic.Int32 + etx := cltest.MustInsertUnconfirmedEthTxWithBroadcastLegacyAttempt(t, txStore, 0, fromAddress) + cltest.MustInsertUnconfirmedEthTxWithBroadcastLegacyAttempt(t, txStore, 1, fromAddress) + cltest.MustInsertUnconfirmedEthTxWithBroadcastLegacyAttempt(t, txStore, 2, fromAddress) + require.NoError(t, txStore.UpdateTxAttemptBroadcastBeforeBlockNum(testutils.Context(t), etx.ID, 7)) - backend := mocks.NewPrometheusBackend(t) + backend := headreporter.NewMockPrometheusBackend(t) backend.On("SetUnconfirmedTransactions", big.NewInt(0), int64(3)).Return() backend.On("SetMaxUnconfirmedAge", big.NewInt(0), mock.MatchedBy(func(s float64) bool { return s > 0 })).Return() backend.On("SetMaxUnconfirmedBlocks", big.NewInt(0), int64(35)).Return() - backend.On("SetPipelineTaskRunsQueued", 0).Return() - backend.On("SetPipelineRunsQueued", 0). - Run(func(args mock.Arguments) { - subscribeCalls.Add(1) - }). - Return() - reporter := promreporter.NewPromReporter(db, newLegacyChainContainer(t, db), logger.TestLogger(t), backend, 10*time.Millisecond) - servicetest.Run(t, reporter) - etx := cltest.MustInsertUnconfirmedEthTxWithBroadcastLegacyAttempt(t, txStore, 0, fromAddress) - cltest.MustInsertUnconfirmedEthTxWithBroadcastLegacyAttempt(t, txStore, 1, fromAddress) - cltest.MustInsertUnconfirmedEthTxWithBroadcastLegacyAttempt(t, txStore, 2, fromAddress) - require.NoError(t, txStore.UpdateTxAttemptBroadcastBeforeBlockNum(testutils.Context(t), etx.ID, 7)) + reporter := headreporter.NewPrometheusReporter(db, newLegacyChainContainer(t, db)) + reporter.SetBackend(backend) + + head := headreporter.NewHead() + err := reporter.ReportNewHead(testutils.Context(t), &head) + require.NoError(t, err) - head := newHead() - reporter.OnNewLongestChain(testutils.Context(t), &head) + backend.On("SetPipelineTaskRunsQueued", 0).Return() + backend.On("SetPipelineRunsQueued", 0).Return() - require.Eventually(t, func() bool { return subscribeCalls.Load() >= 1 }, 12*time.Second, 100*time.Millisecond) + err = reporter.ReportPeriodic(testutils.Context(t)) + require.NoError(t, err) }) t.Run("with unfinished pipeline task runs", func(t *testing.T) { db := pgtest.NewSqlxDB(t) pgtest.MustExec(t, db, `SET CONSTRAINTS pipeline_task_runs_pipeline_run_id_fkey DEFERRED`) - backend := mocks.NewPrometheusBackend(t) - reporter := promreporter.NewPromReporter(db, newLegacyChainContainer(t, db), logger.TestLogger(t), backend, 10*time.Millisecond) - cltest.MustInsertUnfinishedPipelineTaskRun(t, db, 1) cltest.MustInsertUnfinishedPipelineTaskRun(t, db, 1) cltest.MustInsertUnfinishedPipelineTaskRun(t, db, 2) - var subscribeCalls atomic.Int32 - + backend := headreporter.NewMockPrometheusBackend(t) backend.On("SetUnconfirmedTransactions", big.NewInt(0), int64(0)).Return() backend.On("SetMaxUnconfirmedAge", big.NewInt(0), float64(0)).Return() backend.On("SetMaxUnconfirmedBlocks", big.NewInt(0), int64(0)).Return() - backend.On("SetPipelineTaskRunsQueued", 3).Return() - backend.On("SetPipelineRunsQueued", 2). - Run(func(args mock.Arguments) { - subscribeCalls.Add(1) - }). - Return() - servicetest.Run(t, reporter) - head := newHead() - reporter.OnNewLongestChain(testutils.Context(t), &head) + reporter := headreporter.NewPrometheusReporter(db, newLegacyChainContainer(t, db)) + reporter.SetBackend(backend) + + head := headreporter.NewHead() + err := reporter.ReportNewHead(testutils.Context(t), &head) + require.NoError(t, err) + + backend.On("SetPipelineTaskRunsQueued", 3).Return() + backend.On("SetPipelineRunsQueued", 2).Return() - require.Eventually(t, func() bool { return subscribeCalls.Load() >= 1 }, 12*time.Second, 100*time.Millisecond) + err = reporter.ReportPeriodic(testutils.Context(t)) + require.NoError(t, err) }) } + +func newLegacyChainContainer(t *testing.T, db *sqlx.DB) legacyevm.LegacyChainContainer { + config, dbConfig, evmConfig := txmgr.MakeTestConfigs(t) + keyStore := cltest.NewKeyStore(t, db).Eth() + ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + estimator, err := gas.NewEstimator(logger.TestLogger(t), ethClient, config, evmConfig.GasEstimator()) + require.NoError(t, err) + lggr := logger.TestLogger(t) + lpOpts := logpoller.Opts{ + PollPeriod: 100 * time.Millisecond, + FinalityDepth: 2, + BackfillBatchSize: 3, + RpcBatchSize: 2, + KeepFinalizedBlocksDepth: 1000, + } + ht := headtracker.NewSimulatedHeadTracker(ethClient, lpOpts.UseFinalityTag, lpOpts.FinalityDepth) + lp := logpoller.NewLogPoller(logpoller.NewORM(testutils.FixtureChainID, db, lggr), ethClient, lggr, ht, lpOpts) + + txm, err := txmgr.NewTxm( + db, + evmConfig, + evmConfig.GasEstimator(), + evmConfig.Transactions(), + nil, + dbConfig, + dbConfig.Listener(), + ethClient, + lggr, + lp, + keyStore, + estimator) + require.NoError(t, err) + + cfg := configtest.NewGeneralConfig(t, nil) + return cltest.NewLegacyChainsWithMockChainAndTxManager(t, ethClient, cfg, txm) +} diff --git a/core/services/headreporter/telemetry_reporter.go b/core/services/headreporter/telemetry_reporter.go new file mode 100644 index 0000000000..0d93ca59a4 --- /dev/null +++ b/core/services/headreporter/telemetry_reporter.go @@ -0,0 +1,69 @@ +package headreporter + +import ( + "context" + "math/big" + + "github.com/pkg/errors" + + "github.com/smartcontractkit/libocr/commontypes" + "google.golang.org/protobuf/proto" + + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" + "github.com/smartcontractkit/chainlink/v2/core/services/synchronization/telem" + "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" +) + +type telemetryReporter struct { + lggr logger.Logger + endpoints map[uint64]commontypes.MonitoringEndpoint +} + +func NewTelemetryReporter(monitoringEndpointGen telemetry.MonitoringEndpointGenerator, lggr logger.Logger, chainIDs ...*big.Int) HeadReporter { + endpoints := make(map[uint64]commontypes.MonitoringEndpoint) + for _, chainID := range chainIDs { + endpoints[chainID.Uint64()] = monitoringEndpointGen.GenMonitoringEndpoint("EVM", chainID.String(), "", synchronization.HeadReport) + } + return &telemetryReporter{lggr: lggr.Named("TelemetryReporter"), endpoints: endpoints} +} + +func (t *telemetryReporter) ReportNewHead(ctx context.Context, head *evmtypes.Head) error { + monitoringEndpoint := t.endpoints[head.EVMChainID.ToInt().Uint64()] + if monitoringEndpoint == nil { + return errors.Errorf("No monitoring endpoint provided chain_id=%d", head.EVMChainID.Int64()) + } + var finalized *telem.Block + latestFinalizedHead := head.LatestFinalizedHead() + if latestFinalizedHead != nil { + finalized = &telem.Block{ + Timestamp: uint64(latestFinalizedHead.GetTimestamp().UTC().Unix()), + Number: uint64(latestFinalizedHead.BlockNumber()), + Hash: latestFinalizedHead.BlockHash().Hex(), + } + } + request := &telem.HeadReportRequest{ + ChainID: head.EVMChainID.String(), + Latest: &telem.Block{ + Timestamp: uint64(head.Timestamp.UTC().Unix()), + Number: uint64(head.Number), + Hash: head.Hash.Hex(), + }, + Finalized: finalized, + } + bytes, err := proto.Marshal(request) + if err != nil { + return errors.WithMessage(err, "telem.HeadReportRequest marshal error") + } + monitoringEndpoint.SendLog(bytes) + if finalized == nil { + t.lggr.Infow("No finalized block was found", "chainID", head.EVMChainID.Int64(), + "head.number", head.Number, "chainLength", head.ChainLength()) + } + return nil +} + +func (t *telemetryReporter) ReportPeriodic(ctx context.Context) error { + return nil +} diff --git a/core/services/headreporter/telemetry_reporter_test.go b/core/services/headreporter/telemetry_reporter_test.go new file mode 100644 index 0000000000..85bfea5866 --- /dev/null +++ b/core/services/headreporter/telemetry_reporter_test.go @@ -0,0 +1,108 @@ +package headreporter_test + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/proto" + + mocks2 "github.com/smartcontractkit/chainlink/v2/common/types/mocks" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/headreporter" + "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" + "github.com/smartcontractkit/chainlink/v2/core/services/synchronization/telem" + "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" +) + +func Test_TelemetryReporter_NewHead(t *testing.T) { + head := evmtypes.Head{ + Number: 42, + EVMChainID: ubig.NewI(100), + Hash: common.HexToHash("0x1010"), + Timestamp: time.UnixMilli(1000), + IsFinalized: false, + Parent: &evmtypes.Head{ + Number: 41, + Hash: common.HexToHash("0x1009"), + Timestamp: time.UnixMilli(999), + IsFinalized: true, + }, + } + requestBytes, err := proto.Marshal(&telem.HeadReportRequest{ + ChainID: "100", + Latest: &telem.Block{ + Timestamp: uint64(head.Timestamp.UTC().Unix()), + Number: 42, + Hash: head.Hash.Hex(), + }, + Finalized: &telem.Block{ + Timestamp: uint64(head.Parent.Timestamp.UTC().Unix()), + Number: 41, + Hash: head.Parent.Hash.Hex(), + }, + }) + assert.NoError(t, err) + + monitoringEndpoint := mocks2.NewMonitoringEndpoint(t) + monitoringEndpoint.On("SendLog", requestBytes).Return() + + monitoringEndpointGen := telemetry.NewMockMonitoringEndpointGenerator(t) + monitoringEndpointGen. + On("GenMonitoringEndpoint", "EVM", "100", "", synchronization.HeadReport). + Return(monitoringEndpoint) + reporter := headreporter.NewTelemetryReporter(monitoringEndpointGen, logger.TestLogger(t), big.NewInt(100)) + + err = reporter.ReportNewHead(testutils.Context(t), &head) + assert.NoError(t, err) +} + +func Test_TelemetryReporter_NewHeadMissingFinalized(t *testing.T) { + head := evmtypes.Head{ + Number: 42, + EVMChainID: ubig.NewI(100), + Hash: common.HexToHash("0x1010"), + Timestamp: time.UnixMilli(1000), + IsFinalized: false, + } + requestBytes, err := proto.Marshal(&telem.HeadReportRequest{ + ChainID: "100", + Latest: &telem.Block{ + Timestamp: uint64(head.Timestamp.UTC().Unix()), + Number: 42, + Hash: head.Hash.Hex(), + }, + }) + assert.NoError(t, err) + + monitoringEndpoint := mocks2.NewMonitoringEndpoint(t) + monitoringEndpoint.On("SendLog", requestBytes).Return() + + monitoringEndpointGen := telemetry.NewMockMonitoringEndpointGenerator(t) + monitoringEndpointGen. + On("GenMonitoringEndpoint", "EVM", "100", "", synchronization.HeadReport). + Return(monitoringEndpoint) + reporter := headreporter.NewTelemetryReporter(monitoringEndpointGen, logger.TestLogger(t), big.NewInt(100)) + + err = reporter.ReportNewHead(testutils.Context(t), &head) + assert.NoError(t, err) +} + +func Test_TelemetryReporter_NewHead_MissingEndpoint(t *testing.T) { + monitoringEndpointGen := telemetry.NewMockMonitoringEndpointGenerator(t) + monitoringEndpointGen. + On("GenMonitoringEndpoint", "EVM", "100", "", synchronization.HeadReport). + Return(nil) + + reporter := headreporter.NewTelemetryReporter(monitoringEndpointGen, logger.TestLogger(t), big.NewInt(100)) + + head := evmtypes.Head{Number: 42, EVMChainID: ubig.NewI(100)} + + err := reporter.ReportNewHead(testutils.Context(t), &head) + assert.Errorf(t, err, "No monitoring endpoint provided chain_id=100") +} diff --git a/core/services/keeper/upkeep_executer_test.go b/core/services/keeper/upkeep_executer_test.go index c70a92c725..55926242a2 100644 --- a/core/services/keeper/upkeep_executer_test.go +++ b/core/services/keeper/upkeep_executer_test.go @@ -48,7 +48,7 @@ func mockEstimator(t *testing.T) gas.EvmFeeEstimator { // note: estimator will only return 1 of legacy or dynamic fees (not both) // assumed to call legacy estimator only estimator := gasmocks.NewEvmFeeEstimator(t) - estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(gas.EvmFee{ + estimator.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(gas.EvmFee{ Legacy: assets.GWei(60), }, uint32(60), nil) return estimator diff --git a/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go b/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go index f1ca4d91cd..b02b7138a1 100644 --- a/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go +++ b/core/services/ocr2/plugins/ccip/ccipcommit/ocr2_test.go @@ -20,15 +20,13 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/hashutil" "github.com/smartcontractkit/chainlink-common/pkg/merklemulti" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/mocks" mocks2 "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" @@ -45,7 +43,6 @@ import ( ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" - ccipdbmocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdb/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/prices" ) @@ -410,7 +407,9 @@ func TestCommitReportingPlugin_Report(t *testing.T) { evmEstimator := mocks.NewEvmFeeEstimator(t) evmEstimator.On("L1Oracle").Return(nil) - gasPriceEstimator := prices.NewDAGasPriceEstimator(evmEstimator, nil, 2e9, 2e9) // 200% deviation + + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + gasPriceEstimator := prices.NewDAGasPriceEstimator(evmEstimator, nil, 2e9, 2e9, feeEstimatorConfig) // 200% deviation var destTokens []cciptypes.Address for tk := range tc.tokenDecimals { @@ -434,7 +433,7 @@ func TestCommitReportingPlugin_Report(t *testing.T) { })).Return(destDecimals, nil).Maybe() lp := mocks2.NewLogPoller(t) - commitStoreReader, err := v1_2_0.NewCommitStore(logger.TestLogger(t), utils.RandomAddress(), nil, lp) + commitStoreReader, err := v1_2_0.NewCommitStore(logger.TestLogger(t), utils.RandomAddress(), nil, lp, feeEstimatorConfig) assert.NoError(t, err) healthCheck := ccipcachemocks.NewChainHealthcheck(t) @@ -1532,6 +1531,7 @@ func TestCommitReportingPlugin_calculatePriceUpdates(t *testing.T) { nil, tc.daGasPriceDeviationPPB, tc.execGasPriceDeviationPPB, + ccipdatamocks.NewFeeEstimatorConfigReader(t), ) r := &CommitReportingPlugin{ diff --git a/core/services/ocr2/plugins/ccip/ccipexec/initializers.go b/core/services/ocr2/plugins/ccip/ccipexec/initializers.go index c8d079b263..aa42ff2828 100644 --- a/core/services/ocr2/plugins/ccip/ccipexec/initializers.go +++ b/core/services/ocr2/plugins/ccip/ccipexec/initializers.go @@ -18,8 +18,6 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/statuschecker" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" @@ -27,6 +25,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/factory" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/observability" diff --git a/core/services/ocr2/plugins/ccip/ccipexec/ocr2_test.go b/core/services/ocr2/plugins/ccip/ccipexec/ocr2_test.go index 84cb73c664..627afda975 100644 --- a/core/services/ocr2/plugins/ccip/ccipexec/ocr2_test.go +++ b/core/services/ocr2/plugins/ccip/ccipexec/ocr2_test.go @@ -16,17 +16,18 @@ import ( mapset "github.com/deckarep/golang-set/v2" "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2/types" - ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" + "github.com/smartcontractkit/libocr/commontypes" + "github.com/smartcontractkit/libocr/offchainreporting2/types" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" lpMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" @@ -41,8 +42,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/prices" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata" - - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) func TestExecutionReportingPlugin_Observation(t *testing.T) { @@ -428,8 +427,10 @@ func TestExecutionReportingPlugin_buildReport(t *testing.T) { p.metricsCollector = ccip.NoopMetricsCollector p.commitStoreReader = commitStore + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + lp := lpMocks.NewLogPoller(t) - offRampReader, err := v1_0_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lp, nil, nil) + offRampReader, err := v1_0_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lp, nil, nil, feeEstimatorConfig) assert.NoError(t, err) p.offRampReader = offRampReader @@ -1376,7 +1377,9 @@ func Test_prepareTokenExecData(t *testing.T) { } func encodeExecutionReport(t *testing.T, report cciptypes.ExecReport) []byte { - reader, err := v1_2_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, nil, nil, nil) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + reader, err := v1_2_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, nil, nil, nil, feeEstimatorConfig) require.NoError(t, err) ctx := testutils.Context(t) encodedReport, err := reader.EncodeExecutionReport(ctx, report) diff --git a/core/services/ocr2/plugins/ccip/config/type_and_version.go b/core/services/ocr2/plugins/ccip/config/type_and_version.go index fdfd892b08..00e3b26d4e 100644 --- a/core/services/ocr2/plugins/ccip/config/type_and_version.go +++ b/core/services/ocr2/plugins/ccip/config/type_and_version.go @@ -19,6 +19,7 @@ var ( EVM2EVMOffRamp ContractType = "EVM2EVMOffRamp" CommitStore ContractType = "CommitStore" PriceRegistry ContractType = "PriceRegistry" + Unknown ContractType = "Unknown" // 1.0.0 Contracts which have no TypeAndVersion ContractTypes = mapset.NewSet[ContractType]( EVM2EVMOffRamp, EVM2EVMOnRamp, @@ -63,7 +64,13 @@ func TypeAndVersion(addr common.Address, client bind.ContractBackend) (ContractT return ContractType(contractType), *v, nil } +// default version to use when TypeAndVersion is missing. +const defaultVersion = "1.0.0" + func ParseTypeAndVersion(tvStr string) (string, string, error) { + if tvStr == "" { + tvStr = string(Unknown) + " " + defaultVersion + } typeAndVersionValues := strings.Split(tvStr, " ") if len(typeAndVersionValues) < 2 { diff --git a/core/services/ocr2/plugins/ccip/config/type_and_version_test.go b/core/services/ocr2/plugins/ccip/config/type_and_version_test.go new file mode 100644 index 0000000000..f626389dcd --- /dev/null +++ b/core/services/ocr2/plugins/ccip/config/type_and_version_test.go @@ -0,0 +1,49 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseTypeAndVersion(t *testing.T) { + tests := []struct { + name string + input string + expectedType string + expectedVersion string + expectedError string + }{ + { + name: "Valid input", + input: string(EVM2EVMOnRamp) + " 1.2.0", + expectedType: string(EVM2EVMOnRamp), + expectedVersion: "1.2.0", + }, + { + name: "Empty input", + input: "", + expectedType: string(Unknown), + expectedVersion: defaultVersion, + }, + { + name: "Invalid input", + input: "InvalidInput", + expectedError: "invalid type and version InvalidInput", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actualType, actualVersion, err := ParseTypeAndVersion(tc.input) + + if tc.expectedError != "" { + assert.EqualError(t, err, tc.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedType, actualType) + assert.Equal(t, tc.expectedVersion, actualVersion) + } + }) + } +} diff --git a/core/services/ocr2/plugins/ccip/estimatorconfig/config.go b/core/services/ocr2/plugins/ccip/estimatorconfig/config.go new file mode 100644 index 0000000000..4737bd33e8 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/estimatorconfig/config.go @@ -0,0 +1,83 @@ +package estimatorconfig + +import ( + "context" + "math/big" + + "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" +) + +// FeeEstimatorConfigProvider implements abstract storage for the DataAvailability settings in onRamp dynamic Config. +// It's implemented to transfer DA config from different entities offRamp, onRamp, commitStore without injecting the +// strong dependency between modules. ConfigProvider fetch ccip.OnRampReader object reads and returns only relevant +// fields for the daGasEstimator from the encapsulated onRampReader. +type FeeEstimatorConfigProvider interface { + SetOnRampReader(reader ccip.OnRampReader) + AddGasPriceInterceptor(GasPriceInterceptor) + ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (modExecGasPrice, modDAGasPrice *big.Int, err error) + GetDataAvailabilityConfig(ctx context.Context) (destDataAvailabilityOverheadGas, destGasPerDataAvailabilityByte, destDataAvailabilityMultiplierBps int64, err error) +} + +type GasPriceInterceptor interface { + ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (modExecGasPrice, modDAGasPrice *big.Int, err error) +} + +type FeeEstimatorConfigService struct { + onRampReader ccip.OnRampReader + gasPriceInterceptors []GasPriceInterceptor +} + +func NewFeeEstimatorConfigService() *FeeEstimatorConfigService { + return &FeeEstimatorConfigService{} +} + +// SetOnRampReader Sets the onRamp reader instance. +// must be called once for each instance. +func (c *FeeEstimatorConfigService) SetOnRampReader(reader ccip.OnRampReader) { + c.onRampReader = reader +} + +// GetDataAvailabilityConfig Returns dynamic config data availability parameters. +// GetDynamicConfig should be cached in the onRamp reader to avoid unnecessary on-chain calls +func (c *FeeEstimatorConfigService) GetDataAvailabilityConfig(ctx context.Context) (destDataAvailabilityOverheadGas, destGasPerDataAvailabilityByte, destDataAvailabilityMultiplierBps int64, err error) { + if c.onRampReader == nil { + return 0, 0, 0, nil + } + + cfg, err := c.onRampReader.GetDynamicConfig(ctx) + if err != nil { + return 0, 0, 0, err + } + + return int64(cfg.DestDataAvailabilityOverheadGas), + int64(cfg.DestGasPerDataAvailabilityByte), + int64(cfg.DestDataAvailabilityMultiplierBps), + err +} + +// AddGasPriceInterceptor adds price interceptors that can modify gas price. +func (c *FeeEstimatorConfigService) AddGasPriceInterceptor(gpi GasPriceInterceptor) { + if gpi != nil { + c.gasPriceInterceptors = append(c.gasPriceInterceptors, gpi) + } +} + +// ModifyGasPriceComponents applies gasPrice interceptors and returns modified gasPrice. +func (c *FeeEstimatorConfigService) ModifyGasPriceComponents(ctx context.Context, gasPrice, daGasPrice *big.Int) (*big.Int, *big.Int, error) { + if len(c.gasPriceInterceptors) == 0 { + return gasPrice, daGasPrice, nil + } + + // values are mutable, it is necessary to copy the values to protect the arguments from modification. + cpGasPrice := new(big.Int).Set(gasPrice) + cpDAGasPrice := new(big.Int).Set(daGasPrice) + + var err error + for _, interceptor := range c.gasPriceInterceptors { + if cpGasPrice, cpDAGasPrice, err = interceptor.ModifyGasPriceComponents(ctx, cpGasPrice, cpDAGasPrice); err != nil { + return nil, nil, err + } + } + + return cpGasPrice, cpDAGasPrice, nil +} diff --git a/core/services/ocr2/plugins/ccip/estimatorconfig/config_test.go b/core/services/ocr2/plugins/ccip/estimatorconfig/config_test.go new file mode 100644 index 0000000000..3ecd88ae3b --- /dev/null +++ b/core/services/ocr2/plugins/ccip/estimatorconfig/config_test.go @@ -0,0 +1,112 @@ +package estimatorconfig_test + +import ( + "context" + "errors" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + mocks2 "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig/mocks" + + "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" +) + +func TestFeeEstimatorConfigService(t *testing.T) { + svc := estimatorconfig.NewFeeEstimatorConfigService() + ctx := context.Background() + + var expectedDestDataAvailabilityOverheadGas int64 = 1 + var expectedDestGasPerDataAvailabilityByte int64 = 2 + var expectedDestDataAvailabilityMultiplierBps int64 = 3 + + onRampReader := mocks.NewOnRampReader(t) + destDataAvailabilityOverheadGas, destGasPerDataAvailabilityByte, destDataAvailabilityMultiplierBps, err := svc.GetDataAvailabilityConfig(ctx) + require.NoError(t, err) // if onRampReader not set, return nil error and 0 values + require.EqualValues(t, 0, destDataAvailabilityOverheadGas) + require.EqualValues(t, 0, destGasPerDataAvailabilityByte) + require.EqualValues(t, 0, destDataAvailabilityMultiplierBps) + svc.SetOnRampReader(onRampReader) + + onRampReader.On("GetDynamicConfig", ctx). + Return(ccip.OnRampDynamicConfig{ + DestDataAvailabilityOverheadGas: uint32(expectedDestDataAvailabilityOverheadGas), + DestGasPerDataAvailabilityByte: uint16(expectedDestGasPerDataAvailabilityByte), + DestDataAvailabilityMultiplierBps: uint16(expectedDestDataAvailabilityMultiplierBps), + }, nil).Once() + + destDataAvailabilityOverheadGas, destGasPerDataAvailabilityByte, destDataAvailabilityMultiplierBps, err = svc.GetDataAvailabilityConfig(ctx) + require.NoError(t, err) + require.Equal(t, expectedDestDataAvailabilityOverheadGas, destDataAvailabilityOverheadGas) + require.Equal(t, expectedDestGasPerDataAvailabilityByte, destGasPerDataAvailabilityByte) + require.Equal(t, expectedDestDataAvailabilityMultiplierBps, destDataAvailabilityMultiplierBps) + + onRampReader.On("GetDynamicConfig", ctx). + Return(ccip.OnRampDynamicConfig{}, errors.New("test")).Once() + _, _, _, err = svc.GetDataAvailabilityConfig(ctx) + require.Error(t, err) +} + +func TestModifyGasPriceComponents(t *testing.T) { + t.Run("success modification", func(t *testing.T) { + svc := estimatorconfig.NewFeeEstimatorConfigService() + ctx := context.Background() + + initialExecGasPrice, initialDaGasPrice := big.NewInt(10), big.NewInt(1) + + gpi1 := mocks2.NewGasPriceInterceptor(t) + svc.AddGasPriceInterceptor(gpi1) + + // change in first interceptor + firstModExecGasPrice, firstModDaGasPrice := big.NewInt(5), big.NewInt(2) + gpi1.On("ModifyGasPriceComponents", ctx, initialExecGasPrice, initialDaGasPrice). + Return(firstModExecGasPrice, firstModDaGasPrice, nil) + + gpi2 := mocks2.NewGasPriceInterceptor(t) + svc.AddGasPriceInterceptor(gpi2) + + // change in second iterceptor + secondModExecGasPrice, secondModDaGasPrice := big.NewInt(50), big.NewInt(20) + gpi2.On("ModifyGasPriceComponents", ctx, firstModExecGasPrice, firstModDaGasPrice). + Return(secondModExecGasPrice, secondModDaGasPrice, nil) + + // has to return second interceptor values + resGasPrice, resDAGasPrice, err := svc.ModifyGasPriceComponents(ctx, initialExecGasPrice, initialDaGasPrice) + require.NoError(t, err) + require.Equal(t, secondModExecGasPrice.Int64(), resGasPrice.Int64()) + require.Equal(t, secondModDaGasPrice.Int64(), resDAGasPrice.Int64()) + }) + + t.Run("error modification", func(t *testing.T) { + svc := estimatorconfig.NewFeeEstimatorConfigService() + ctx := context.Background() + + initialExecGasPrice, initialDaGasPrice := big.NewInt(10), big.NewInt(1) + gpi1 := mocks2.NewGasPriceInterceptor(t) + svc.AddGasPriceInterceptor(gpi1) + gpi1.On("ModifyGasPriceComponents", ctx, initialExecGasPrice, initialDaGasPrice). + Return(nil, nil, errors.New("test")) + + // has to return second interceptor values + _, _, err := svc.ModifyGasPriceComponents(ctx, initialExecGasPrice, initialDaGasPrice) + require.Error(t, err) + }) + + t.Run("without interceptors", func(t *testing.T) { + svc := estimatorconfig.NewFeeEstimatorConfigService() + ctx := context.Background() + + initialExecGasPrice, initialDaGasPrice := big.NewInt(10), big.NewInt(1) + + // has to return second interceptor values + resGasPrice, resDAGasPrice, err := svc.ModifyGasPriceComponents(ctx, initialExecGasPrice, initialDaGasPrice) + require.NoError(t, err) + + // values should not be modified + require.Equal(t, initialExecGasPrice, resGasPrice) + require.Equal(t, initialDaGasPrice, resDAGasPrice) + }) +} diff --git a/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor.go b/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor.go new file mode 100644 index 0000000000..8d5d3bb6c2 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor.go @@ -0,0 +1,80 @@ +package mantle + +import ( + "context" + "fmt" + "math/big" + "strings" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + evmClient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" +) + +const ( + // tokenRatio is not volatile and can be requested not often. + tokenRatioUpdateInterval = 60 * time.Minute + // tokenRatio fetches the tokenRatio used for Mantle's gas price calculation + tokenRatioMethod = "tokenRatio" + mantleTokenRatioAbiString = `[{"inputs":[],"name":"tokenRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` +) + +type Interceptor struct { + client evmClient.Client + tokenRatioCallData []byte + tokenRatio *big.Int + tokenRatioLastUpdate time.Time +} + +func NewInterceptor(_ context.Context, client evmClient.Client) (*Interceptor, error) { + // Encode calldata for tokenRatio method + tokenRatioMethodAbi, err := abi.JSON(strings.NewReader(mantleTokenRatioAbiString)) + if err != nil { + return nil, fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for Mantle; %v", tokenRatioMethod, err) + } + tokenRatioCallData, err := tokenRatioMethodAbi.Pack(tokenRatioMethod) + if err != nil { + return nil, fmt.Errorf("failed to parse GasPriceOracle %s() calldata for Mantle; %v", tokenRatioMethod, err) + } + + return &Interceptor{ + client: client, + tokenRatioCallData: tokenRatioCallData, + }, nil +} + +// ModifyGasPriceComponents returns modified gasPrice. +func (i *Interceptor) ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (*big.Int, *big.Int, error) { + if time.Since(i.tokenRatioLastUpdate) > tokenRatioUpdateInterval { + mantleTokenRatio, err := i.getMantleTokenRatio(ctx) + if err != nil { + return nil, nil, err + } + + i.tokenRatio, i.tokenRatioLastUpdate = mantleTokenRatio, time.Now() + } + + // multiply daGasPrice and execGas price by tokenRatio + newExecGasPrice := new(big.Int).Mul(execGasPrice, i.tokenRatio) + newDAGasPrice := new(big.Int).Mul(daGasPrice, i.tokenRatio) + return newExecGasPrice, newDAGasPrice, nil +} + +// getMantleTokenRatio Requests and returns a token ratio value for the Mantle chain. +func (i *Interceptor) getMantleTokenRatio(ctx context.Context) (*big.Int, error) { + precompile := common.HexToAddress(rollups.OPGasOracleAddress) + tokenRatio, err := i.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: i.tokenRatioCallData, + }, nil) + + if err != nil { + return nil, fmt.Errorf("getMantleTokenRatio call failed: %w", err) + } + + return new(big.Int).SetBytes(tokenRatio), nil +} diff --git a/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor_test.go b/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor_test.go new file mode 100644 index 0000000000..9134d996c2 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle/interceptor_test.go @@ -0,0 +1,96 @@ +package mantle + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" +) + +func TestInterceptor(t *testing.T) { + ethClient := mocks.NewClient(t) + ctx := context.Background() + + tokenRatio := big.NewInt(10) + interceptor, err := NewInterceptor(ctx, ethClient) + require.NoError(t, err) + + // request token ratio + ethClient.On("CallContract", ctx, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})). + Return(common.BigToHash(tokenRatio).Bytes(), nil).Once() + + modExecGasPrice, modDAGasPrice, err := interceptor.ModifyGasPriceComponents(ctx, big.NewInt(1), big.NewInt(1)) + require.NoError(t, err) + require.Equal(t, int64(10), modExecGasPrice.Int64()) + require.Equal(t, int64(10), modDAGasPrice.Int64()) + + // second call won't invoke eth client + modExecGasPrice, modDAGasPrice, err = interceptor.ModifyGasPriceComponents(ctx, big.NewInt(2), big.NewInt(1)) + require.NoError(t, err) + require.Equal(t, int64(20), modExecGasPrice.Int64()) + require.Equal(t, int64(10), modDAGasPrice.Int64()) +} + +func TestModifyGasPriceComponents(t *testing.T) { + testCases := map[string]struct { + execGasPrice *big.Int + daGasPrice *big.Int + tokenRatio *big.Int + resultExecGasPrice *big.Int + resultDAGasPrice *big.Int + }{ + "regular": { + execGasPrice: big.NewInt(1000), + daGasPrice: big.NewInt(100), + resultExecGasPrice: big.NewInt(2000), + resultDAGasPrice: big.NewInt(200), + tokenRatio: big.NewInt(2), + }, + "zero DAGasPrice": { + execGasPrice: big.NewInt(1000), + daGasPrice: big.NewInt(0), + resultExecGasPrice: big.NewInt(5000), + resultDAGasPrice: big.NewInt(0), + tokenRatio: big.NewInt(5), + }, + "zero ExecGasPrice": { + execGasPrice: big.NewInt(0), + daGasPrice: big.NewInt(10), + resultExecGasPrice: big.NewInt(0), + resultDAGasPrice: big.NewInt(50), + tokenRatio: big.NewInt(5), + }, + "zero token ratio": { + execGasPrice: big.NewInt(15), + daGasPrice: big.NewInt(10), + resultExecGasPrice: big.NewInt(0), + resultDAGasPrice: big.NewInt(0), + tokenRatio: big.NewInt(0), + }, + } + + for tcName, tc := range testCases { + t.Run(tcName, func(t *testing.T) { + ethClient := mocks.NewClient(t) + ctx := context.Background() + + interceptor, err := NewInterceptor(ctx, ethClient) + require.NoError(t, err) + + // request token ratio + ethClient.On("CallContract", ctx, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})). + Return(common.BigToHash(tc.tokenRatio).Bytes(), nil).Once() + + modExecGasPrice, modDAGasPrice, err := interceptor.ModifyGasPriceComponents(ctx, tc.execGasPrice, tc.daGasPrice) + require.NoError(t, err) + require.Equal(t, tc.resultExecGasPrice.Int64(), modExecGasPrice.Int64()) + require.Equal(t, tc.resultDAGasPrice.Int64(), modDAGasPrice.Int64()) + }) + } +} diff --git a/core/services/ocr2/plugins/ccip/estimatorconfig/mocks/gas_price_interceptor_mock.go b/core/services/ocr2/plugins/ccip/estimatorconfig/mocks/gas_price_interceptor_mock.go new file mode 100644 index 0000000000..62ec5a2207 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/estimatorconfig/mocks/gas_price_interceptor_mock.go @@ -0,0 +1,106 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + mock "github.com/stretchr/testify/mock" +) + +// GasPriceInterceptor is an autogenerated mock type for the GasPriceInterceptor type +type GasPriceInterceptor struct { + mock.Mock +} + +type GasPriceInterceptor_Expecter struct { + mock *mock.Mock +} + +func (_m *GasPriceInterceptor) EXPECT() *GasPriceInterceptor_Expecter { + return &GasPriceInterceptor_Expecter{mock: &_m.Mock} +} + +// ModifyGasPriceComponents provides a mock function with given fields: ctx, execGasPrice, daGasPrice +func (_m *GasPriceInterceptor) ModifyGasPriceComponents(ctx context.Context, execGasPrice *big.Int, daGasPrice *big.Int) (*big.Int, *big.Int, error) { + ret := _m.Called(ctx, execGasPrice, daGasPrice) + + if len(ret) == 0 { + panic("no return value specified for ModifyGasPriceComponents") + } + + var r0 *big.Int + var r1 *big.Int + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *big.Int, *big.Int) (*big.Int, *big.Int, error)); ok { + return rf(ctx, execGasPrice, daGasPrice) + } + if rf, ok := ret.Get(0).(func(context.Context, *big.Int, *big.Int) *big.Int); ok { + r0 = rf(ctx, execGasPrice, daGasPrice) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *big.Int, *big.Int) *big.Int); ok { + r1 = rf(ctx, execGasPrice, daGasPrice) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*big.Int) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, *big.Int, *big.Int) error); ok { + r2 = rf(ctx, execGasPrice, daGasPrice) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GasPriceInterceptor_ModifyGasPriceComponents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ModifyGasPriceComponents' +type GasPriceInterceptor_ModifyGasPriceComponents_Call struct { + *mock.Call +} + +// ModifyGasPriceComponents is a helper method to define mock.On call +// - ctx context.Context +// - execGasPrice *big.Int +// - daGasPrice *big.Int +func (_e *GasPriceInterceptor_Expecter) ModifyGasPriceComponents(ctx interface{}, execGasPrice interface{}, daGasPrice interface{}) *GasPriceInterceptor_ModifyGasPriceComponents_Call { + return &GasPriceInterceptor_ModifyGasPriceComponents_Call{Call: _e.mock.On("ModifyGasPriceComponents", ctx, execGasPrice, daGasPrice)} +} + +func (_c *GasPriceInterceptor_ModifyGasPriceComponents_Call) Run(run func(ctx context.Context, execGasPrice *big.Int, daGasPrice *big.Int)) *GasPriceInterceptor_ModifyGasPriceComponents_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*big.Int), args[2].(*big.Int)) + }) + return _c +} + +func (_c *GasPriceInterceptor_ModifyGasPriceComponents_Call) Return(modExecGasPrice *big.Int, modDAGasPrice *big.Int, err error) *GasPriceInterceptor_ModifyGasPriceComponents_Call { + _c.Call.Return(modExecGasPrice, modDAGasPrice, err) + return _c +} + +func (_c *GasPriceInterceptor_ModifyGasPriceComponents_Call) RunAndReturn(run func(context.Context, *big.Int, *big.Int) (*big.Int, *big.Int, error)) *GasPriceInterceptor_ModifyGasPriceComponents_Call { + _c.Call.Return(run) + return _c +} + +// NewGasPriceInterceptor creates a new instance of GasPriceInterceptor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewGasPriceInterceptor(t interface { + mock.TestingT + Cleanup(func()) +}) *GasPriceInterceptor { + mock := &GasPriceInterceptor{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/services/ocr2/plugins/ccip/exportinternal.go b/core/services/ocr2/plugins/ccip/exportinternal.go index 2a5767ac85..2f924085fb 100644 --- a/core/services/ocr2/plugins/ccip/exportinternal.go +++ b/core/services/ocr2/plugins/ccip/exportinternal.go @@ -37,20 +37,20 @@ func NewEvmPriceRegistry(lp logpoller.LogPoller, ec client.Client, lggr logger.L type VersionFinder = factory.VersionFinder -func NewCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address ccip.Address, ec client.Client, lp logpoller.LogPoller) (ccipdata.CommitStoreReader, error) { - return factory.NewCommitStoreReader(lggr, versionFinder, address, ec, lp) +func NewCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address ccip.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (ccipdata.CommitStoreReader, error) { + return factory.NewCommitStoreReader(lggr, versionFinder, address, ec, lp, feeEstimatorConfig) } -func CloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address ccip.Address, ec client.Client, lp logpoller.LogPoller) error { - return factory.CloseCommitStoreReader(lggr, versionFinder, address, ec, lp) +func CloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address ccip.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) error { + return factory.CloseCommitStoreReader(lggr, versionFinder, address, ec, lp, feeEstimatorConfig) } -func NewOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr ccip.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, registerFilters bool) (ccipdata.OffRampReader, error) { - return factory.NewOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, registerFilters) +func NewOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr ccip.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, registerFilters bool, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (ccipdata.OffRampReader, error) { + return factory.NewOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, registerFilters, feeEstimatorConfig) } -func CloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr ccip.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int) error { - return factory.CloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice) +func CloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr ccip.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) error { + return factory.CloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, feeEstimatorConfig) } func NewEvmVersionFinder() factory.EvmVersionFinder { diff --git a/core/services/ocr2/plugins/ccip/internal/cache/commit_roots_test.go b/core/services/ocr2/plugins/ccip/internal/cache/commit_roots_test.go index dc0a844349..d8289212e8 100644 --- a/core/services/ocr2/plugins/ccip/internal/cache/commit_roots_test.go +++ b/core/services/ocr2/plugins/ccip/internal/cache/commit_roots_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/require" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" @@ -18,6 +17,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" ) @@ -54,7 +54,9 @@ func Test_RootsEligibleForExecution(t *testing.T) { } require.NoError(t, orm.InsertLogsWithBlock(ctx, inputLogs, logpoller.NewLogPollerBlock(utils.RandomBytes32(), 2, time.Now(), 1))) - commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp, feeEstimatorConfig) require.NoError(t, err) rootsCache := cache.NewCommitRootsCache(logger.TestLogger(t), commitStore, 10*time.Hour, time.Second) @@ -162,7 +164,9 @@ func Test_RootsEligibleForExecutionWithReorgs(t *testing.T) { } require.NoError(t, orm.InsertLogsWithBlock(ctx, inputLogs, logpoller.NewLogPollerBlock(utils.RandomBytes32(), 3, time.Now(), 1))) - commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp, feeEstimatorConfig) require.NoError(t, err) rootsCache := cache.NewCommitRootsCache(logger.TestLogger(t), commitStore, 10*time.Hour, time.Second) @@ -221,7 +225,9 @@ func Test_BlocksWithTheSameTimestamps(t *testing.T) { } require.NoError(t, orm.InsertLogsWithBlock(ctx, inputLogs, logpoller.NewLogPollerBlock(utils.RandomBytes32(), 2, time.Now(), 2))) - commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + commitStore, err := v1_2_0.NewCommitStore(logger.TestLogger(t), commitStoreAddr, nil, lp, feeEstimatorConfig) require.NoError(t, err) rootsCache := cache.NewCommitRootsCache(logger.TestLogger(t), commitStore, 10*time.Hour, time.Second) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go index 7f6b1c1bb7..81807e66fe 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/commit_store_reader_test.go @@ -13,7 +13,6 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/config" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" @@ -38,6 +37,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/factory" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" ) @@ -193,15 +193,24 @@ func TestCommitStoreReaders(t *testing.T) { lm := new(rollupMocks.L1Oracle) ge.On("L1Oracle").Return(lm) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + feeEstimatorConfig.On( + "ModifyGasPriceComponents", + mock.AnythingOfType("context.backgroundCtx"), + mock.AnythingOfType("*big.Int"), + mock.AnythingOfType("*big.Int"), + ).Return(func(ctx context.Context, x, y *big.Int) (*big.Int, *big.Int, error) { + return x, y, nil + }) maxGasPrice := big.NewInt(1e8) - c10r, err := factory.NewCommitStoreReader(lggr, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(addr), ec, lp) // ge, maxGasPrice + c10r, err := factory.NewCommitStoreReader(lggr, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(addr), ec, lp, feeEstimatorConfig) // ge, maxGasPrice require.NoError(t, err) err = c10r.SetGasEstimator(ctx, ge) require.NoError(t, err) err = c10r.SetSourceMaxGasPrice(ctx, maxGasPrice) require.NoError(t, err) assert.Equal(t, reflect.TypeOf(c10r).String(), reflect.TypeOf(&v1_0_0.CommitStore{}).String()) - c12r, err := factory.NewCommitStoreReader(lggr, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(addr2), ec, lp) + c12r, err := factory.NewCommitStoreReader(lggr, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(addr2), ec, lp, feeEstimatorConfig) require.NoError(t, err) err = c12r.SetGasEstimator(ctx, ge) require.NoError(t, err) @@ -284,7 +293,7 @@ func TestCommitStoreReaders(t *testing.T) { } gasPrice := big.NewInt(10) daPrice := big.NewInt(20) - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, assets.NewWei(maxGasPrice)).Return(gas.EvmFee{Legacy: assets.NewWei(gasPrice)}, uint64(0), nil) + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, assets.NewWei(maxGasPrice), (*common.Address)(nil), (*common.Address)(nil)).Return(gas.EvmFee{Legacy: assets.NewWei(gasPrice)}, uint64(0), nil) lm.On("GasPrice", mock.Anything).Return(assets.NewWei(daPrice), nil) for v, cr := range crs { @@ -370,8 +379,10 @@ func TestCommitStoreReaders(t *testing.T) { require.NoError(t, err) assert.Equal(t, commonOffchain, c2) // We should be able to query for gas prices now. + gpe, err := cr.GasPriceEstimator(ctx) require.NoError(t, err) + gp, err := gpe.GetGasPrice(context.Background()) require.NoError(t, err) assert.True(t, gp.Cmp(big.NewInt(0)) > 0) @@ -412,7 +423,10 @@ func TestNewCommitStoreReader(t *testing.T) { if tc.expectedErr == "" { lp.On("RegisterFilter", mock.Anything, mock.Anything).Return(nil) } - _, err = factory.NewCommitStoreReader(logger.TestLogger(t), factory.NewEvmVersionFinder(), addr, c, lp) + + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + _, err = factory.NewCommitStoreReader(logger.TestLogger(t), factory.NewEvmVersionFinder(), addr, c, lp, feeEstimatorConfig) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store.go index d431d2863a..c5f32ba91c 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store.go @@ -21,16 +21,16 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0" ) -func NewCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller) (ccipdata.CommitStoreReader, error) { - return initOrCloseCommitStoreReader(lggr, versionFinder, address, ec, lp, false) +func NewCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (ccipdata.CommitStoreReader, error) { + return initOrCloseCommitStoreReader(lggr, versionFinder, address, ec, lp, feeEstimatorConfig, false) } -func CloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller) error { - _, err := initOrCloseCommitStoreReader(lggr, versionFinder, address, ec, lp, true) +func CloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) error { + _, err := initOrCloseCommitStoreReader(lggr, versionFinder, address, ec, lp, feeEstimatorConfig, true) return err } -func initOrCloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller, closeReader bool) (ccipdata.CommitStoreReader, error) { +func initOrCloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, closeReader bool) (ccipdata.CommitStoreReader, error) { contractType, version, err := versionFinder.TypeAndVersion(address, ec) if err != nil { return nil, errors.Wrapf(err, "unable to read type and version") @@ -57,7 +57,7 @@ func initOrCloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinde } return cs, cs.RegisterFilters() case ccipdata.V1_2_0: - cs, err := v1_2_0.NewCommitStore(lggr, evmAddr, ec, lp) + cs, err := v1_2_0.NewCommitStore(lggr, evmAddr, ec, lp, feeEstimatorConfig) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func initOrCloseCommitStoreReader(lggr logger.Logger, versionFinder VersionFinde } return cs, cs.RegisterFilters() case ccipdata.V1_5_0: - cs, err := v1_5_0.NewCommitStore(lggr, evmAddr, ec, lp) + cs, err := v1_5_0.NewCommitStore(lggr, evmAddr, ec, lp, feeEstimatorConfig) if err != nil { return nil, err } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store_test.go index e1b8ff929c..987b6f848e 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/commit_store_test.go @@ -8,13 +8,13 @@ import ( "github.com/stretchr/testify/mock" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" mocks2 "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" "github.com/smartcontractkit/chainlink/v2/core/logger" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" ) @@ -24,14 +24,16 @@ func TestCommitStore(t *testing.T) { addr := cciptypes.Address(utils.RandomAddress().String()) lp := mocks2.NewLogPoller(t) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + lp.On("RegisterFilter", mock.Anything, mock.Anything).Return(nil) versionFinder := newMockVersionFinder(ccipconfig.CommitStore, *semver.MustParse(versionStr), nil) - _, err := NewCommitStoreReader(lggr, versionFinder, addr, nil, lp) + _, err := NewCommitStoreReader(lggr, versionFinder, addr, nil, lp, feeEstimatorConfig) assert.NoError(t, err) expFilterName := logpoller.FilterName(v1_0_0.EXEC_REPORT_ACCEPTS, addr) lp.On("UnregisterFilter", mock.Anything, expFilterName).Return(nil) - err = CloseCommitStoreReader(lggr, versionFinder, addr, nil, lp) + err = CloseCommitStoreReader(lggr, versionFinder, addr, nil, lp, feeEstimatorConfig) assert.NoError(t, err) } } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp.go index c6fa63ee82..5d9b751d0b 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp.go @@ -26,16 +26,16 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0" ) -func NewOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, registerFilters bool) (ccipdata.OffRampReader, error) { - return initOrCloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, false, registerFilters) +func NewOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, registerFilters bool, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (ccipdata.OffRampReader, error) { + return initOrCloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, false, registerFilters, feeEstimatorConfig) } -func CloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int) error { - _, err := initOrCloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, true, false) +func CloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) error { + _, err := initOrCloseOffRampReader(lggr, versionFinder, addr, destClient, lp, estimator, destMaxGasPrice, true, false, feeEstimatorConfig) return err } -func initOrCloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, closeReader bool, registerFilters bool) (ccipdata.OffRampReader, error) { +func initOrCloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, addr cciptypes.Address, destClient client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, closeReader bool, registerFilters bool, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (ccipdata.OffRampReader, error) { contractType, version, err := versionFinder.TypeAndVersion(addr, destClient) if err != nil { return nil, errors.Wrapf(err, "unable to read type and version") @@ -53,7 +53,7 @@ func initOrCloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, a switch version.String() { case ccipdata.V1_0_0, ccipdata.V1_1_0: - offRamp, err := v1_0_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice) + offRamp, err := v1_0_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice, feeEstimatorConfig) if err != nil { return nil, err } @@ -62,7 +62,7 @@ func initOrCloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, a } return offRamp, offRamp.RegisterFilters() case ccipdata.V1_2_0: - offRamp, err := v1_2_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice) + offRamp, err := v1_2_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice, feeEstimatorConfig) if err != nil { return nil, err } @@ -71,7 +71,7 @@ func initOrCloseOffRampReader(lggr logger.Logger, versionFinder VersionFinder, a } return offRamp, offRamp.RegisterFilters() case ccipdata.V1_5_0: - offRamp, err := v1_5_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice) + offRamp, err := v1_5_0.NewOffRamp(lggr, evmAddr, destClient, lp, estimator, destMaxGasPrice, feeEstimatorConfig) if err != nil { return nil, err } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp_test.go index 4b9e57ecfb..c00d9e134b 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/offramp_test.go @@ -15,6 +15,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" ) @@ -24,6 +25,8 @@ func TestOffRamp(t *testing.T) { addr := cciptypes.Address(utils.RandomAddress().String()) lp := mocks2.NewLogPoller(t) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + expFilterNames := []string{ logpoller.FilterName(v1_0_0.EXEC_EXECUTION_STATE_CHANGES, addr), logpoller.FilterName(v1_0_0.EXEC_TOKEN_POOL_ADDED, addr), @@ -32,13 +35,13 @@ func TestOffRamp(t *testing.T) { versionFinder := newMockVersionFinder(ccipconfig.EVM2EVMOffRamp, *semver.MustParse(versionStr), nil) lp.On("RegisterFilter", mock.Anything, mock.Anything).Return(nil).Times(len(expFilterNames)) - _, err := NewOffRampReader(lggr, versionFinder, addr, nil, lp, nil, nil, true) + _, err := NewOffRampReader(lggr, versionFinder, addr, nil, lp, nil, nil, true, feeEstimatorConfig) assert.NoError(t, err) for _, f := range expFilterNames { lp.On("UnregisterFilter", mock.Anything, f).Return(nil) } - err = CloseOffRampReader(lggr, versionFinder, addr, nil, lp, nil, nil) + err = CloseOffRampReader(lggr, versionFinder, addr, nil, lp, nil, nil, feeEstimatorConfig) assert.NoError(t, err) } } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/fee_estimator_config.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/fee_estimator_config.go new file mode 100644 index 0000000000..0b4d1ce75a --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/fee_estimator_config.go @@ -0,0 +1,11 @@ +package ccipdata + +import ( + "context" + "math/big" +) + +type FeeEstimatorConfigReader interface { + GetDataAvailabilityConfig(ctx context.Context) (destDAOverheadGas, destGasPerDAByte, destDAMultiplierBps int64, err error) + ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (modExecGasPrice, modDAGasPrice *big.Int, err error) +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks/fee_estimator_config_mock.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks/fee_estimator_config_mock.go new file mode 100644 index 0000000000..e62cdec87a --- /dev/null +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks/fee_estimator_config_mock.go @@ -0,0 +1,177 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + big "math/big" + + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// FeeEstimatorConfigReader is an autogenerated mock type for the FeeEstimatorConfigReader type +type FeeEstimatorConfigReader struct { + mock.Mock +} + +type FeeEstimatorConfigReader_Expecter struct { + mock *mock.Mock +} + +func (_m *FeeEstimatorConfigReader) EXPECT() *FeeEstimatorConfigReader_Expecter { + return &FeeEstimatorConfigReader_Expecter{mock: &_m.Mock} +} + +// GetDataAvailabilityConfig provides a mock function with given fields: ctx +func (_m *FeeEstimatorConfigReader) GetDataAvailabilityConfig(ctx context.Context) (int64, int64, int64, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetDataAvailabilityConfig") + } + + var r0 int64 + var r1 int64 + var r2 int64 + var r3 error + if rf, ok := ret.Get(0).(func(context.Context) (int64, int64, int64, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) int64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context) int64); ok { + r1 = rf(ctx) + } else { + r1 = ret.Get(1).(int64) + } + + if rf, ok := ret.Get(2).(func(context.Context) int64); ok { + r2 = rf(ctx) + } else { + r2 = ret.Get(2).(int64) + } + + if rf, ok := ret.Get(3).(func(context.Context) error); ok { + r3 = rf(ctx) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDataAvailabilityConfig' +type FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call struct { + *mock.Call +} + +// GetDataAvailabilityConfig is a helper method to define mock.On call +// - ctx context.Context +func (_e *FeeEstimatorConfigReader_Expecter) GetDataAvailabilityConfig(ctx interface{}) *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call { + return &FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call{Call: _e.mock.On("GetDataAvailabilityConfig", ctx)} +} + +func (_c *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call) Run(run func(ctx context.Context)) *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call) Return(destDAOverheadGas int64, destGasPerDAByte int64, destDAMultiplierBps int64, err error) *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call { + _c.Call.Return(destDAOverheadGas, destGasPerDAByte, destDAMultiplierBps, err) + return _c +} + +func (_c *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call) RunAndReturn(run func(context.Context) (int64, int64, int64, error)) *FeeEstimatorConfigReader_GetDataAvailabilityConfig_Call { + _c.Call.Return(run) + return _c +} + +// ModifyGasPriceComponents provides a mock function with given fields: ctx, execGasPrice, daGasPrice +func (_m *FeeEstimatorConfigReader) ModifyGasPriceComponents(ctx context.Context, execGasPrice *big.Int, daGasPrice *big.Int) (*big.Int, *big.Int, error) { + ret := _m.Called(ctx, execGasPrice, daGasPrice) + + if len(ret) == 0 { + panic("no return value specified for ModifyGasPriceComponents") + } + + var r0 *big.Int + var r1 *big.Int + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *big.Int, *big.Int) (*big.Int, *big.Int, error)); ok { + return rf(ctx, execGasPrice, daGasPrice) + } + if rf, ok := ret.Get(0).(func(context.Context, *big.Int, *big.Int) *big.Int); ok { + r0 = rf(ctx, execGasPrice, daGasPrice) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *big.Int, *big.Int) *big.Int); ok { + r1 = rf(ctx, execGasPrice, daGasPrice) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*big.Int) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, *big.Int, *big.Int) error); ok { + r2 = rf(ctx, execGasPrice, daGasPrice) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// FeeEstimatorConfigReader_ModifyGasPriceComponents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ModifyGasPriceComponents' +type FeeEstimatorConfigReader_ModifyGasPriceComponents_Call struct { + *mock.Call +} + +// ModifyGasPriceComponents is a helper method to define mock.On call +// - ctx context.Context +// - execGasPrice *big.Int +// - daGasPrice *big.Int +func (_e *FeeEstimatorConfigReader_Expecter) ModifyGasPriceComponents(ctx interface{}, execGasPrice interface{}, daGasPrice interface{}) *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call { + return &FeeEstimatorConfigReader_ModifyGasPriceComponents_Call{Call: _e.mock.On("ModifyGasPriceComponents", ctx, execGasPrice, daGasPrice)} +} + +func (_c *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call) Run(run func(ctx context.Context, execGasPrice *big.Int, daGasPrice *big.Int)) *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*big.Int), args[2].(*big.Int)) + }) + return _c +} + +func (_c *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call) Return(modExecGasPrice *big.Int, modDAGasPrice *big.Int, err error) *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call { + _c.Call.Return(modExecGasPrice, modDAGasPrice, err) + return _c +} + +func (_c *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call) RunAndReturn(run func(context.Context, *big.Int, *big.Int) (*big.Int, *big.Int, error)) *FeeEstimatorConfigReader_ModifyGasPriceComponents_Call { + _c.Call.Return(run) + return _c +} + +// NewFeeEstimatorConfigReader creates a new instance of FeeEstimatorConfigReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFeeEstimatorConfigReader(t interface { + mock.TestingT + Cleanup(func()) +}) *FeeEstimatorConfigReader { + mock := &FeeEstimatorConfigReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/offramp_reader_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/offramp_reader_test.go index f5711422bd..df405a5a61 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/offramp_reader_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/offramp_reader_test.go @@ -13,7 +13,6 @@ import ( "github.com/stretchr/testify/require" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmclientmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker" @@ -32,6 +31,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/factory" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" ) @@ -194,8 +194,10 @@ func setupOffRampReaderTH(t *testing.T, version string) offRampReaderTH { require.Fail(t, "Unknown version: ", version) } + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + // Create the version-specific reader. - reader, err := factory.NewOffRampReader(log, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(offRampAddress), bc, lp, nil, nil, true) + reader, err := factory.NewOffRampReader(log, factory.NewEvmVersionFinder(), ccipcalc.EvmAddrToGeneric(offRampAddress), bc, lp, nil, nil, true, feeEstimatorConfig) require.NoError(t, err) addr, err := reader.Address(ctx) require.NoError(t, err) @@ -401,11 +403,14 @@ func TestNewOffRampReader(t *testing.T) { b, err := utils.ABIEncode(`[{"type":"string"}]`, tc.typeAndVersion) require.NoError(t, err) c := evmclientmocks.NewClient(t) + + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + c.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(b, nil) addr := ccipcalc.EvmAddrToGeneric(utils.RandomAddress()) lp := lpmocks.NewLogPoller(t) lp.On("RegisterFilter", mock.Anything, mock.Anything).Return(nil).Maybe() - _, err = factory.NewOffRampReader(logger.TestLogger(t), factory.NewEvmVersionFinder(), addr, c, lp, nil, nil, true) + _, err = factory.NewOffRampReader(logger.TestLogger(t), factory.NewEvmVersionFinder(), addr, c, lp, nil, nil, true, feeEstimatorConfig) if tc.expectedErr != "" { assert.EqualError(t, err, tc.expectedErr) } else { diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp.go index 137cbaf451..b5625c59d0 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp.go @@ -156,6 +156,7 @@ type OffRamp struct { eventSig common.Hash cachedOffRampTokens cache.AutoSync[cciptypes.OffRampTokens] sourceToDestTokensCache sync.Map + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader // Dynamic config // configMu guards all the dynamic config fields. @@ -627,7 +628,7 @@ func (o *OffRamp) RegisterFilters() error { return logpollerutil.RegisterLpFilters(o.lp, o.filters) } -func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int) (*OffRamp, error) { +func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (*OffRamp, error) { offRamp, err := evm_2_evm_offramp_1_0_0.NewEVM2EVMOffRamp(addr, ec) if err != nil { return nil, err @@ -682,8 +683,9 @@ func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp lo offRamp.Address(), ), // values set on the fly after ChangeConfig is called - gasPriceEstimator: prices.ExecGasPriceEstimator{}, - offchainConfig: cciptypes.ExecOffchainConfig{}, - onchainConfig: cciptypes.ExecOnchainConfig{}, + gasPriceEstimator: prices.ExecGasPriceEstimator{}, + offchainConfig: cciptypes.ExecOffchainConfig{}, + onchainConfig: cciptypes.ExecOnchainConfig{}, + feeEstimatorConfig: feeEstimatorConfig, }, nil } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_test.go index d834b792ce..455c1dbcb8 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_test.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0" ) @@ -25,7 +26,9 @@ func TestExecutionReportEncodingV100(t *testing.T) { ProofFlagBits: big.NewInt(133), } - offRamp, err := v1_0_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lpmocks.NewLogPoller(t), nil, nil) + feeEstimatorConfig := mocks.NewFeeEstimatorConfigReader(t) + + offRamp, err := v1_0_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lpmocks.NewLogPoller(t), nil, nil, feeEstimatorConfig) require.NoError(t, err) ctx := testutils.Context(t) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_unit_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_unit_test.go index f8b1dc4e61..6cde3753b7 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_unit_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/offramp_reader_unit_test.go @@ -6,16 +6,12 @@ import ( "slices" "testing" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib/rpclibmocks" - "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - evmclimocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" @@ -27,6 +23,9 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/cache" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib/rpclibmocks" ) func TestOffRampGetDestinationTokensFromSourceTokens(t *testing.T) { @@ -190,13 +189,15 @@ func Test_LogsAreProperlyMarkedAsFinalized(t *testing.T) { t.Run(tt.name, func(t *testing.T) { offrampAddress := utils.RandomAddress() + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + lp := mocks.NewLogPoller(t) lp.On("LatestBlock", mock.Anything). Return(logpoller.LogPollerBlock{FinalizedBlockNumber: int64(tt.lastFinalizedBlock)}, nil) lp.On("IndexedLogsTopicRange", mock.Anything, ExecutionStateChangedEvent, offrampAddress, 1, logpoller.EvmWord(minSeqNr), logpoller.EvmWord(maxSeqNr), evmtypes.Confirmations(0)). Return(inputLogs, nil) - offRamp, err := NewOffRamp(logger.TestLogger(t), offrampAddress, evmclimocks.NewClient(t), lp, nil, nil) + offRamp, err := NewOffRamp(logger.TestLogger(t), offrampAddress, evmclimocks.NewClient(t), lp, nil, nil, feeEstimatorConfig) require.NoError(t, err) logs, err := offRamp.GetExecutionStateChangesBetweenSeqNums(testutils.Context(t), minSeqNr, maxSeqNr, 0) require.NoError(t, err) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/onramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/onramp.go index 6737abe64c..d6f3094af7 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/onramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_0_0/onramp.go @@ -30,16 +30,16 @@ const ( var _ ccipdata.OnRampReader = &OnRamp{} type OnRamp struct { - address common.Address - onRamp *evm_2_evm_onramp_1_0_0.EVM2EVMOnRamp - lp logpoller.LogPoller - lggr logger.Logger - client client.Client - leafHasher ccipdata.LeafHasherInterface[[32]byte] - sendRequestedEventSig common.Hash - sendRequestedSeqNumberWord int - filters []logpoller.Filter - cachedSourcePriceRegistryAddress cache.AutoSync[cciptypes.Address] + address common.Address + onRamp *evm_2_evm_onramp_1_0_0.EVM2EVMOnRamp + lp logpoller.LogPoller + lggr logger.Logger + client client.Client + leafHasher ccipdata.LeafHasherInterface[[32]byte] + sendRequestedEventSig common.Hash + sendRequestedSeqNumberWord int + filters []logpoller.Filter + cachedOnRampDynamicConfig cache.AutoSync[cciptypes.OnRampDynamicConfig] // Static config can be cached, because it's never expected to change. // The only way to change that is through the contract's constructor (redeployment) cachedStaticConfig cache.OnceCtxFunction[evm_2_evm_onramp_1_0_0.EVM2EVMOnRampStaticConfig] @@ -90,7 +90,7 @@ func NewOnRamp(lggr logger.Logger, sourceSelector, destSelector uint64, onRampAd // offset || sourceChainID || seqNum || ... sendRequestedSeqNumberWord: 2, sendRequestedEventSig: eventSig, - cachedSourcePriceRegistryAddress: cache.NewLogpollerEventsBased[cciptypes.Address]( + cachedOnRampDynamicConfig: cache.NewLogpollerEventsBased[cciptypes.OnRampDynamicConfig]( sourceLP, []common.Hash{configSetEventSig}, onRampAddress, @@ -104,38 +104,38 @@ func (o *OnRamp) Address(context.Context) (cciptypes.Address, error) { return cciptypes.Address(o.onRamp.Address().String()), nil } -func (o *OnRamp) GetDynamicConfig(context.Context) (cciptypes.OnRampDynamicConfig, error) { - if o.onRamp == nil { - return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") - } - legacyDynamicConfig, err := o.onRamp.GetDynamicConfig(nil) - if err != nil { - return cciptypes.OnRampDynamicConfig{}, err - } - return cciptypes.OnRampDynamicConfig{ - Router: cciptypes.Address(legacyDynamicConfig.Router.String()), - MaxNumberOfTokensPerMsg: legacyDynamicConfig.MaxTokensLength, - DestGasOverhead: 0, - DestGasPerPayloadByte: 0, - DestDataAvailabilityOverheadGas: 0, - DestGasPerDataAvailabilityByte: 0, - DestDataAvailabilityMultiplierBps: 0, - PriceRegistry: cciptypes.Address(legacyDynamicConfig.PriceRegistry.String()), - MaxDataBytes: legacyDynamicConfig.MaxDataSize, - MaxPerMsgGasLimit: uint32(legacyDynamicConfig.MaxGasLimit), - }, nil -} - -func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { - return o.cachedSourcePriceRegistryAddress.Get(ctx, func(ctx context.Context) (cciptypes.Address, error) { - c, err := o.GetDynamicConfig(ctx) +func (o *OnRamp) GetDynamicConfig(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + return o.cachedOnRampDynamicConfig.Get(ctx, func(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + if o.onRamp == nil { + return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") + } + legacyDynamicConfig, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{Context: ctx}) if err != nil { - return "", err + return cciptypes.OnRampDynamicConfig{}, err } - return c.PriceRegistry, nil + return cciptypes.OnRampDynamicConfig{ + Router: cciptypes.Address(legacyDynamicConfig.Router.String()), + MaxNumberOfTokensPerMsg: legacyDynamicConfig.MaxTokensLength, + DestGasOverhead: 0, + DestGasPerPayloadByte: 0, + DestDataAvailabilityOverheadGas: 0, + DestGasPerDataAvailabilityByte: 0, + DestDataAvailabilityMultiplierBps: 0, + PriceRegistry: cciptypes.Address(legacyDynamicConfig.PriceRegistry.String()), + MaxDataBytes: legacyDynamicConfig.MaxDataSize, + MaxPerMsgGasLimit: uint32(legacyDynamicConfig.MaxGasLimit), + }, nil }) } +func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { + c, err := o.GetDynamicConfig(ctx) + if err != nil { + return "", err + } + return c.PriceRegistry, nil +} + func (o *OnRamp) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, seqNumMax uint64, finalized bool) ([]cciptypes.EVM2EVMMessageWithTxMeta, error) { logs, err := o.lp.LogsDataWordRange( ctx, @@ -165,8 +165,8 @@ func (o *OnRamp) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, s return res, nil } -func (o *OnRamp) RouterAddress(context.Context) (cciptypes.Address, error) { - config, err := o.onRamp.GetDynamicConfig(nil) +func (o *OnRamp) RouterAddress(ctx context.Context) (cciptypes.Address, error) { + config, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{Context: ctx}) if err != nil { return "", err } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store.go index 7612e54419..ecc8acb576 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store.go @@ -49,9 +49,10 @@ type CommitStore struct { commitReportArgs abi.Arguments // Dynamic config - configMu sync.RWMutex - gasPriceEstimator *prices.DAGasPriceEstimator - offchainConfig cciptypes.CommitOffchainConfig + configMu sync.RWMutex + gasPriceEstimator *prices.DAGasPriceEstimator + offchainConfig cciptypes.CommitOffchainConfig + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader } func (c *CommitStore) GetCommitStoreStaticConfig(ctx context.Context) (cciptypes.CommitStoreStaticConfig, error) { @@ -255,6 +256,7 @@ func (c *CommitStore) ChangeConfig(_ context.Context, onchainConfig []byte, offc c.sourceMaxGasPrice, int64(offchainConfigParsed.ExecGasPriceDeviationPPB), int64(offchainConfigParsed.DAGasPriceDeviationPPB), + c.feeEstimatorConfig, ) c.offchainConfig = ccipdata.NewCommitOffchainConfig( offchainConfigParsed.ExecGasPriceDeviationPPB, @@ -430,7 +432,7 @@ func (c *CommitStore) RegisterFilters() error { return logpollerutil.RegisterLpFilters(c.lp, c.filters) } -func NewCommitStore(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller) (*CommitStore, error) { +func NewCommitStore(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller, feeEstimatorConfig ccipdata.FeeEstimatorConfigReader) (*CommitStore, error) { commitStore, err := commit_store_1_2_0.NewCommitStore(addr, ec) if err != nil { return nil, err @@ -463,7 +465,8 @@ func NewCommitStore(lggr logger.Logger, addr common.Address, ec client.Client, l configMu: sync.RWMutex{}, // The fields below are initially empty and set on ChangeConfig method - offchainConfig: cciptypes.CommitOffchainConfig{}, - gasPriceEstimator: nil, + offchainConfig: cciptypes.CommitOffchainConfig{}, + gasPriceEstimator: nil, + feeEstimatorConfig: feeEstimatorConfig, }, nil } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store_test.go index 8b29309633..4307be0353 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/commit_store_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/config" - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" @@ -18,6 +17,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" ) func TestCommitReportEncoding(t *testing.T) { @@ -48,7 +48,9 @@ func TestCommitReportEncoding(t *testing.T) { Interval: cciptypes.CommitStoreInterval{Min: 1, Max: 10}, } - c, err := NewCommitStore(logger.TestLogger(t), utils.RandomAddress(), nil, mocks.NewLogPoller(t)) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + c, err := NewCommitStore(logger.TestLogger(t), utils.RandomAddress(), nil, mocks.NewLogPoller(t), feeEstimatorConfig) assert.NoError(t, err) encodedReport, err := c.EncodeCommitReport(ctx, report) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp.go index fa00894b38..f853adfb6f 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp.go @@ -123,7 +123,8 @@ func (c JSONExecOffchainConfig) Validate() error { // OffRamp In 1.2 we have a different estimator impl type OffRamp struct { *v1_0_0.OffRamp - offRampV120 evm_2_evm_offramp_1_2_0.EVM2EVMOffRampInterface + offRampV120 evm_2_evm_offramp_1_2_0.EVM2EVMOffRampInterface + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader } func (o *OffRamp) CurrentRateLimiterState(ctx context.Context) (cciptypes.TokenBucketRateLimit, error) { @@ -180,7 +181,7 @@ func (o *OffRamp) ChangeConfig(ctx context.Context, onchainConfigBytes []byte, o PermissionLessExecutionThresholdSeconds: time.Second * time.Duration(onchainConfigParsed.PermissionLessExecutionThresholdSeconds), Router: cciptypes.Address(onchainConfigParsed.Router.String()), } - priceEstimator := prices.NewDAGasPriceEstimator(o.Estimator, o.DestMaxGasPrice, 0, 0) + priceEstimator := prices.NewDAGasPriceEstimator(o.Estimator, o.DestMaxGasPrice, 0, 0, o.feeEstimatorConfig) o.UpdateDynamicConfig(onchainConfig, offchainConfig, priceEstimator) @@ -320,8 +321,16 @@ func (o *OffRamp) DecodeExecutionReport(ctx context.Context, report []byte) (cci return DecodeExecReport(ctx, o.ExecutionReportArgs, report) } -func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int) (*OffRamp, error) { - v100, err := v1_0_0.NewOffRamp(lggr, addr, ec, lp, estimator, destMaxGasPrice) +func NewOffRamp( + lggr logger.Logger, + addr common.Address, + ec client.Client, + lp logpoller.LogPoller, + estimator gas.EvmFeeEstimator, + destMaxGasPrice *big.Int, + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, +) (*OffRamp, error) { + v100, err := v1_0_0.NewOffRamp(lggr, addr, ec, lp, estimator, destMaxGasPrice, feeEstimatorConfig) if err != nil { return nil, err } @@ -334,7 +343,8 @@ func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp lo v100.ExecutionReportArgs = abihelpers.MustGetMethodInputs("manuallyExecute", abiOffRamp)[:1] return &OffRamp{ - OffRamp: v100, - offRampV120: offRamp, + OffRamp: v100, + offRampV120: offRamp, + feeEstimatorConfig: feeEstimatorConfig, }, nil } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp_reader_test.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp_reader_test.go index f87fc8842f..c298349261 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp_reader_test.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/offramp_reader_test.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" ) @@ -25,7 +26,9 @@ func TestExecutionReportEncodingV120(t *testing.T) { ProofFlagBits: big.NewInt(133), } - offRamp, err := v1_2_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lpmocks.NewLogPoller(t), nil, nil) + feeEstimatorConfig := mocks.NewFeeEstimatorConfigReader(t) + + offRamp, err := v1_2_0.NewOffRamp(logger.TestLogger(t), utils.RandomAddress(), nil, lpmocks.NewLogPoller(t), nil, nil, feeEstimatorConfig) require.NoError(t, err) ctx := testutils.Context(t) diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/onramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/onramp.go index 2939682347..2de2b104c9 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/onramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0/onramp.go @@ -49,16 +49,16 @@ var _ ccipdata.OnRampReader = &OnRamp{} // Significant change in 1.2: // - CCIPSendRequested event signature has changed type OnRamp struct { - onRamp *evm_2_evm_onramp_1_2_0.EVM2EVMOnRamp - address common.Address - lggr logger.Logger - lp logpoller.LogPoller - leafHasher ccipdata.LeafHasherInterface[[32]byte] - client client.Client - sendRequestedEventSig common.Hash - sendRequestedSeqNumberWord int - filters []logpoller.Filter - cachedSourcePriceRegistryAddress cache.AutoSync[cciptypes.Address] + onRamp *evm_2_evm_onramp_1_2_0.EVM2EVMOnRamp + address common.Address + lggr logger.Logger + lp logpoller.LogPoller + leafHasher ccipdata.LeafHasherInterface[[32]byte] + client client.Client + sendRequestedEventSig common.Hash + sendRequestedSeqNumberWord int + filters []logpoller.Filter + cachedOnRampDynamicConfig cache.AutoSync[cciptypes.OnRampDynamicConfig] // Static config can be cached, because it's never expected to change. // The only way to change that is through the contract's constructor (redeployment) cachedStaticConfig cache.OnceCtxFunction[evm_2_evm_onramp_1_2_0.EVM2EVMOnRampStaticConfig] @@ -107,7 +107,7 @@ func NewOnRamp(lggr logger.Logger, sourceSelector, destSelector uint64, onRampAd address: onRampAddress, sendRequestedSeqNumberWord: CCIPSendRequestSeqNumIndex, sendRequestedEventSig: CCIPSendRequestEventSig, - cachedSourcePriceRegistryAddress: cache.NewLogpollerEventsBased[cciptypes.Address]( + cachedOnRampDynamicConfig: cache.NewLogpollerEventsBased[cciptypes.OnRampDynamicConfig]( sourceLP, []common.Hash{ConfigSetEventSig}, onRampAddress, @@ -121,38 +121,39 @@ func (o *OnRamp) Address(context.Context) (cciptypes.Address, error) { return cciptypes.Address(o.onRamp.Address().String()), nil } -func (o *OnRamp) GetDynamicConfig(context.Context) (cciptypes.OnRampDynamicConfig, error) { - if o.onRamp == nil { - return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") - } - config, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{}) - if err != nil { - return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("get dynamic config v1.2: %w", err) - } - return cciptypes.OnRampDynamicConfig{ - Router: cciptypes.Address(config.Router.String()), - MaxNumberOfTokensPerMsg: config.MaxNumberOfTokensPerMsg, - DestGasOverhead: config.DestGasOverhead, - DestGasPerPayloadByte: config.DestGasPerPayloadByte, - DestDataAvailabilityOverheadGas: config.DestDataAvailabilityOverheadGas, - DestGasPerDataAvailabilityByte: config.DestGasPerDataAvailabilityByte, - DestDataAvailabilityMultiplierBps: config.DestDataAvailabilityMultiplierBps, - PriceRegistry: cciptypes.Address(config.PriceRegistry.String()), - MaxDataBytes: config.MaxDataBytes, - MaxPerMsgGasLimit: config.MaxPerMsgGasLimit, - }, nil -} - -func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { - return o.cachedSourcePriceRegistryAddress.Get(ctx, func(ctx context.Context) (cciptypes.Address, error) { - c, err := o.GetDynamicConfig(ctx) +func (o *OnRamp) GetDynamicConfig(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + return o.cachedOnRampDynamicConfig.Get(ctx, func(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + if o.onRamp == nil { + return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") + } + config, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{Context: ctx}) if err != nil { - return "", err + return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("get dynamic config v1.2: %w", err) } - return c.PriceRegistry, nil + + return cciptypes.OnRampDynamicConfig{ + Router: cciptypes.Address(config.Router.String()), + MaxNumberOfTokensPerMsg: config.MaxNumberOfTokensPerMsg, + DestGasOverhead: config.DestGasOverhead, + DestGasPerPayloadByte: config.DestGasPerPayloadByte, + DestDataAvailabilityOverheadGas: config.DestDataAvailabilityOverheadGas, + DestGasPerDataAvailabilityByte: config.DestGasPerDataAvailabilityByte, + DestDataAvailabilityMultiplierBps: config.DestDataAvailabilityMultiplierBps, + PriceRegistry: cciptypes.Address(config.PriceRegistry.String()), + MaxDataBytes: config.MaxDataBytes, + MaxPerMsgGasLimit: config.MaxPerMsgGasLimit, + }, nil }) } +func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { + c, err := o.GetDynamicConfig(ctx) + if err != nil { + return "", err + } + return c.PriceRegistry, nil +} + func (o *OnRamp) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, seqNumMax uint64, finalized bool) ([]cciptypes.EVM2EVMMessageWithTxMeta, error) { logs, err := o.lp.LogsDataWordRange( ctx, diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/commit_store.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/commit_store.go index 3bb582f3a2..d5545174cb 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/commit_store.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/commit_store.go @@ -3,6 +3,8 @@ package v1_5_0 import ( "context" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -41,8 +43,14 @@ func (c *CommitStore) IsDown(ctx context.Context) (bool, error) { return !unPausedAndNotCursed, nil } -func NewCommitStore(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller) (*CommitStore, error) { - v120, err := v1_2_0.NewCommitStore(lggr, addr, ec, lp) +func NewCommitStore( + lggr logger.Logger, + addr common.Address, + ec client.Client, + lp logpoller.LogPoller, + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, +) (*CommitStore, error) { + v120, err := v1_2_0.NewCommitStore(lggr, addr, ec, lp, feeEstimatorConfig) if err != nil { return nil, err } diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/offramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/offramp.go index 15f52bd713..11e7be1a55 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/offramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/offramp.go @@ -70,6 +70,7 @@ type OffRamp struct { *v1_2_0.OffRamp offRampV150 evm_2_evm_offramp.EVM2EVMOffRampInterface cachedRateLimitTokens cache.AutoSync[cciptypes.OffRampTokens] + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader } // GetTokens Returns no data as the offRamps no longer have this information. @@ -155,7 +156,7 @@ func (o *OffRamp) ChangeConfig(ctx context.Context, onchainConfigBytes []byte, o PermissionLessExecutionThresholdSeconds: time.Second * time.Duration(onchainConfigParsed.PermissionLessExecutionThresholdSeconds), Router: cciptypes.Address(onchainConfigParsed.Router.String()), } - priceEstimator := prices.NewDAGasPriceEstimator(o.Estimator, o.DestMaxGasPrice, 0, 0) + priceEstimator := prices.NewDAGasPriceEstimator(o.Estimator, o.DestMaxGasPrice, 0, 0, o.feeEstimatorConfig) o.UpdateDynamicConfig(onchainConfig, offchainConfig, priceEstimator) @@ -166,8 +167,16 @@ func (o *OffRamp) ChangeConfig(ctx context.Context, onchainConfigBytes []byte, o cciptypes.Address(destWrappedNative.String()), nil } -func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp logpoller.LogPoller, estimator gas.EvmFeeEstimator, destMaxGasPrice *big.Int) (*OffRamp, error) { - v120, err := v1_2_0.NewOffRamp(lggr, addr, ec, lp, estimator, destMaxGasPrice) +func NewOffRamp( + lggr logger.Logger, + addr common.Address, + ec client.Client, + lp logpoller.LogPoller, + estimator gas.EvmFeeEstimator, + destMaxGasPrice *big.Int, + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, +) (*OffRamp, error) { + v120, err := v1_2_0.NewOffRamp(lggr, addr, ec, lp, estimator, destMaxGasPrice, feeEstimatorConfig) if err != nil { return nil, err } @@ -180,8 +189,9 @@ func NewOffRamp(lggr logger.Logger, addr common.Address, ec client.Client, lp lo v120.ExecutionReportArgs = abihelpers.MustGetMethodInputs("manuallyExecute", abiOffRamp)[:1] return &OffRamp{ - OffRamp: v120, - offRampV150: offRamp, + feeEstimatorConfig: feeEstimatorConfig, + OffRamp: v120, + offRampV150: offRamp, cachedRateLimitTokens: cache.NewLogpollerEventsBased[cciptypes.OffRampTokens]( lp, []common.Hash{RateLimitTokenAddedEvent, RateLimitTokenRemovedEvent}, diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/onramp.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/onramp.go index 354a5defdd..5a9377858d 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/onramp.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_5_0/onramp.go @@ -50,17 +50,17 @@ func init() { var _ ccipdata.OnRampReader = &OnRamp{} type OnRamp struct { - onRamp *evm_2_evm_onramp.EVM2EVMOnRamp - address common.Address - destChainSelectorBytes [16]byte - lggr logger.Logger - lp logpoller.LogPoller - leafHasher ccipdata.LeafHasherInterface[[32]byte] - client client.Client - sendRequestedEventSig common.Hash - sendRequestedSeqNumberWord int - filters []logpoller.Filter - cachedSourcePriceRegistryAddress cache.AutoSync[cciptypes.Address] + onRamp *evm_2_evm_onramp.EVM2EVMOnRamp + address common.Address + destChainSelectorBytes [16]byte + lggr logger.Logger + lp logpoller.LogPoller + leafHasher ccipdata.LeafHasherInterface[[32]byte] + client client.Client + sendRequestedEventSig common.Hash + sendRequestedSeqNumberWord int + filters []logpoller.Filter + cachedOnRampDynamicConfig cache.AutoSync[cciptypes.OnRampDynamicConfig] // Static config can be cached, because it's never expected to change. // The only way to change that is through the contract's constructor (redeployment) cachedStaticConfig cache.OnceCtxFunction[evm_2_evm_onramp.EVM2EVMOnRampStaticConfig] @@ -112,7 +112,7 @@ func NewOnRamp(lggr logger.Logger, sourceSelector, destSelector uint64, onRampAd address: onRampAddress, sendRequestedSeqNumberWord: CCIPSendRequestSeqNumIndex, sendRequestedEventSig: CCIPSendRequestEventSig, - cachedSourcePriceRegistryAddress: cache.NewLogpollerEventsBased[cciptypes.Address]( + cachedOnRampDynamicConfig: cache.NewLogpollerEventsBased[cciptypes.OnRampDynamicConfig]( sourceLP, []common.Hash{ConfigSetEventSig}, onRampAddress, @@ -126,38 +126,39 @@ func (o *OnRamp) Address(context.Context) (cciptypes.Address, error) { return ccipcalc.EvmAddrToGeneric(o.onRamp.Address()), nil } -func (o *OnRamp) GetDynamicConfig(context.Context) (cciptypes.OnRampDynamicConfig, error) { - if o.onRamp == nil { - return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") - } - config, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{}) - if err != nil { - return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("get dynamic config v1.5: %w", err) - } - return cciptypes.OnRampDynamicConfig{ - Router: ccipcalc.EvmAddrToGeneric(config.Router), - MaxNumberOfTokensPerMsg: config.MaxNumberOfTokensPerMsg, - DestGasOverhead: config.DestGasOverhead, - DestGasPerPayloadByte: config.DestGasPerPayloadByte, - DestDataAvailabilityOverheadGas: config.DestDataAvailabilityOverheadGas, - DestGasPerDataAvailabilityByte: config.DestGasPerDataAvailabilityByte, - DestDataAvailabilityMultiplierBps: config.DestDataAvailabilityMultiplierBps, - PriceRegistry: ccipcalc.EvmAddrToGeneric(config.PriceRegistry), - MaxDataBytes: config.MaxDataBytes, - MaxPerMsgGasLimit: config.MaxPerMsgGasLimit, - }, nil -} - -func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { - return o.cachedSourcePriceRegistryAddress.Get(ctx, func(ctx context.Context) (cciptypes.Address, error) { - c, err := o.GetDynamicConfig(ctx) +func (o *OnRamp) GetDynamicConfig(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + return o.cachedOnRampDynamicConfig.Get(ctx, func(ctx context.Context) (cciptypes.OnRampDynamicConfig, error) { + if o.onRamp == nil { + return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("onramp not initialized") + } + config, err := o.onRamp.GetDynamicConfig(&bind.CallOpts{}) if err != nil { - return "", err + return cciptypes.OnRampDynamicConfig{}, fmt.Errorf("get dynamic config v1.5: %w", err) } - return c.PriceRegistry, nil + + return cciptypes.OnRampDynamicConfig{ + Router: ccipcalc.EvmAddrToGeneric(config.Router), + MaxNumberOfTokensPerMsg: config.MaxNumberOfTokensPerMsg, + DestGasOverhead: config.DestGasOverhead, + DestGasPerPayloadByte: config.DestGasPerPayloadByte, + DestDataAvailabilityOverheadGas: config.DestDataAvailabilityOverheadGas, + DestGasPerDataAvailabilityByte: config.DestGasPerDataAvailabilityByte, + DestDataAvailabilityMultiplierBps: config.DestDataAvailabilityMultiplierBps, + PriceRegistry: ccipcalc.EvmAddrToGeneric(config.PriceRegistry), + MaxDataBytes: config.MaxDataBytes, + MaxPerMsgGasLimit: config.MaxPerMsgGasLimit, + }, nil }) } +func (o *OnRamp) SourcePriceRegistryAddress(ctx context.Context) (cciptypes.Address, error) { + c, err := o.GetDynamicConfig(ctx) + if err != nil { + return "", err + } + return c.PriceRegistry, nil +} + func (o *OnRamp) GetSendRequestsBetweenSeqNums(ctx context.Context, seqNumMin, seqNumMax uint64, finalized bool) ([]cciptypes.EVM2EVMMessageWithTxMeta, error) { logs, err := o.lp.LogsDataWordRange( ctx, diff --git a/core/services/ocr2/plugins/ccip/prices/da_price_estimator.go b/core/services/ocr2/plugins/ccip/prices/da_price_estimator.go index 7c75b9bdd9..34239398c8 100644 --- a/core/services/ocr2/plugins/ccip/prices/da_price_estimator.go +++ b/core/services/ocr2/plugins/ccip/prices/da_price_estimator.go @@ -5,6 +5,8 @@ import ( "fmt" "math/big" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" @@ -14,11 +16,9 @@ import ( type DAGasPriceEstimator struct { execEstimator GasPriceEstimator l1Oracle rollups.L1Oracle + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader priceEncodingLength uint daDeviationPPB int64 - daOverheadGas int64 - gasPerDAByte int64 - daMultiplier int64 } func NewDAGasPriceEstimator( @@ -26,12 +26,14 @@ func NewDAGasPriceEstimator( maxGasPrice *big.Int, deviationPPB int64, daDeviationPPB int64, + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, // DA Config Cache updates in the onRamp reader and shares the state ) *DAGasPriceEstimator { return &DAGasPriceEstimator{ execEstimator: NewExecGasPriceEstimator(estimator, maxGasPrice, deviationPPB), l1Oracle: estimator.L1Oracle(), priceEncodingLength: daGasPriceEncodingLength, daDeviationPPB: daDeviationPPB, + feeEstimatorConfig: feeEstimatorConfig, } } @@ -41,6 +43,7 @@ func (g DAGasPriceEstimator) GetGasPrice(ctx context.Context) (*big.Int, error) return nil, err } var gasPrice *big.Int = execGasPrice + if gasPrice.BitLen() > int(g.priceEncodingLength) { return nil, fmt.Errorf("native gas price exceeded max range %+v", gasPrice) } @@ -54,7 +57,14 @@ func (g DAGasPriceEstimator) GetGasPrice(ctx context.Context) (*big.Int, error) return nil, err } - if daGasPrice := daGasPriceWei.ToInt(); daGasPrice.Cmp(big.NewInt(0)) > 0 { + daGasPrice := daGasPriceWei.ToInt() + + gasPrice, daGasPrice, err = g.feeEstimatorConfig.ModifyGasPriceComponents(ctx, gasPrice, daGasPrice) + if err != nil { + return nil, fmt.Errorf("gasPrice modification failed: %v", err) + } + + if daGasPrice.Cmp(big.NewInt(0)) > 0 { if daGasPrice.BitLen() > int(g.priceEncodingLength) { return nil, fmt.Errorf("data availability gas price exceeded max range %+v", daGasPrice) } @@ -141,7 +151,10 @@ func (g DAGasPriceEstimator) EstimateMsgCostUSD(p *big.Int, wrappedNativePrice * // If there is data availability price component, then include data availability cost in fee estimation if daGasPrice.Cmp(big.NewInt(0)) > 0 { - daGasCostUSD := g.estimateDACostUSD(daGasPrice, wrappedNativePrice, msg) + daGasCostUSD, err := g.estimateDACostUSD(daGasPrice, wrappedNativePrice, msg) + if err != nil { + return nil, err + } execCostUSD = new(big.Int).Add(daGasCostUSD, execCostUSD) } return execCostUSD, nil @@ -160,17 +173,22 @@ func (g DAGasPriceEstimator) parseEncodedGasPrice(p *big.Int) (*big.Int, *big.In return daGasPrice, execGasPrice, nil } -func (g DAGasPriceEstimator) estimateDACostUSD(daGasPrice *big.Int, wrappedNativePrice *big.Int, msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta) *big.Int { +func (g DAGasPriceEstimator) estimateDACostUSD(daGasPrice *big.Int, wrappedNativePrice *big.Int, msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta) (*big.Int, error) { var sourceTokenDataLen int for _, tokenData := range msg.SourceTokenData { sourceTokenDataLen += len(tokenData) } + daOverheadGas, gasPerDAByte, daMultiplier, err := g.feeEstimatorConfig.GetDataAvailabilityConfig(context.Background()) + if err != nil { + return nil, err + } + dataLen := evmMessageFixedBytes + len(msg.Data) + len(msg.TokenAmounts)*evmMessageBytesPerToken + sourceTokenDataLen - dataGas := big.NewInt(int64(dataLen)*g.gasPerDAByte + g.daOverheadGas) + dataGas := big.NewInt(int64(dataLen)*gasPerDAByte + daOverheadGas) dataGasEstimate := new(big.Int).Mul(dataGas, daGasPrice) - dataGasEstimate = new(big.Int).Div(new(big.Int).Mul(dataGasEstimate, big.NewInt(g.daMultiplier)), big.NewInt(daMultiplierBase)) + dataGasEstimate = new(big.Int).Div(new(big.Int).Mul(dataGasEstimate, big.NewInt(daMultiplier)), big.NewInt(daMultiplierBase)) - return ccipcalc.CalculateUsdPerUnitGas(dataGasEstimate, wrappedNativePrice) + return ccipcalc.CalculateUsdPerUnitGas(dataGasEstimate, wrappedNativePrice), nil } diff --git a/core/services/ocr2/plugins/ccip/prices/da_price_estimator_test.go b/core/services/ocr2/plugins/ccip/prices/da_price_estimator_test.go index 2f8616a866..52ef8c3800 100644 --- a/core/services/ocr2/plugins/ccip/prices/da_price_estimator_test.go +++ b/core/services/ocr2/plugins/ccip/prices/da_price_estimator_test.go @@ -2,6 +2,7 @@ package prices import ( "context" + "errors" "math/big" "testing" @@ -11,6 +12,7 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups/mocks" + ccipdatamocks "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/mocks" ) func encodeGasPrice(daPrice, execPrice *big.Int) *big.Int { @@ -21,11 +23,13 @@ func TestDAPriceEstimator_GetGasPrice(t *testing.T) { ctx := context.Background() testCases := []struct { - name string - daGasPrice *big.Int - execGasPrice *big.Int - expPrice *big.Int - expErr bool + name string + daGasPrice *big.Int + execGasPrice *big.Int + expPrice *big.Int + modExecGasPrice *big.Int + modDAGasPrice *big.Int + expErr bool }{ { name: "base", @@ -55,6 +59,31 @@ func TestDAPriceEstimator_GetGasPrice(t *testing.T) { expPrice: encodeGasPrice(big.NewInt(1e9), big.NewInt(0)), expErr: false, }, + { + name: "execGasPrice Modified", + daGasPrice: big.NewInt(1e9), + execGasPrice: big.NewInt(0), + modExecGasPrice: big.NewInt(1), + expPrice: encodeGasPrice(big.NewInt(1e9), big.NewInt(1)), + expErr: false, + }, + { + name: "daGasPrice Modified", + daGasPrice: big.NewInt(1e9), + execGasPrice: big.NewInt(0), + modDAGasPrice: big.NewInt(1), + expPrice: encodeGasPrice(big.NewInt(1), big.NewInt(0)), + expErr: false, + }, + { + name: "daGasPrice and execGasPrice Modified", + daGasPrice: big.NewInt(1e9), + execGasPrice: big.NewInt(0), + modDAGasPrice: big.NewInt(1), + modExecGasPrice: big.NewInt(2), + expPrice: encodeGasPrice(big.NewInt(1), big.NewInt(2)), + expErr: false, + }, { name: "price out of bounds", daGasPrice: new(big.Int).Lsh(big.NewInt(1), daGasPriceEncodingLength), @@ -72,10 +101,25 @@ func TestDAPriceEstimator_GetGasPrice(t *testing.T) { l1Oracle := mocks.NewL1Oracle(t) l1Oracle.On("GasPrice", ctx).Return(assets.NewWei(tc.daGasPrice), nil) + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + + modRespExecGasPrice := tc.execGasPrice + if tc.modExecGasPrice != nil { + modRespExecGasPrice = tc.modExecGasPrice + } + + modRespDAGasPrice := tc.daGasPrice + if tc.modDAGasPrice != nil { + modRespDAGasPrice = tc.modDAGasPrice + } + feeEstimatorConfig.On("ModifyGasPriceComponents", mock.Anything, tc.execGasPrice, tc.daGasPrice). + Return(modRespExecGasPrice, modRespDAGasPrice, nil) + g := DAGasPriceEstimator{ execEstimator: execEstimator, l1Oracle: l1Oracle, priceEncodingLength: daGasPriceEncodingLength, + feeEstimatorConfig: feeEstimatorConfig, } gasPrice, err := g.GetGasPrice(ctx) @@ -325,14 +369,17 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { execCostUSD := big.NewInt(100_000) testCases := []struct { - name string - gasPrice *big.Int - wrappedNativePrice *big.Int - msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta - daOverheadGas int64 - gasPerDAByte int64 - daMultiplier int64 - expUSD *big.Int + name string + gasPrice *big.Int + wrappedNativePrice *big.Int + msg cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta + daOverheadGas int64 + gasPerDAByte int64 + daMultiplier int64 + expUSD *big.Int + onRampConfig cciptypes.OnRampDynamicConfig + execEstimatorResponse []any + execEstimatorErr error }{ { name: "only DA overhead", @@ -345,10 +392,8 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { SourceTokenData: [][]byte{}, }, }, - daOverheadGas: 100_000, - gasPerDAByte: 0, - daMultiplier: 10_000, // 1x multiplier - expUSD: new(big.Int).Add(execCostUSD, big.NewInt(100_000e9)), + expUSD: new(big.Int).Add(execCostUSD, big.NewInt(100_000e9)), + execEstimatorResponse: []any{int64(100_000), int64(0), int64(10_000), nil}, }, { name: "include message data gas", @@ -363,10 +408,8 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { }, }, }, - daOverheadGas: 100_000, - gasPerDAByte: 16, - daMultiplier: 10_000, // 1x multiplier - expUSD: new(big.Int).Add(execCostUSD, big.NewInt(134_208e9)), + expUSD: new(big.Int).Add(execCostUSD, big.NewInt(134_208e9)), + execEstimatorResponse: []any{int64(100_000), int64(16), int64(10_000), nil}, }, { name: "zero DA price", @@ -379,10 +422,7 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { SourceTokenData: [][]byte{}, }, }, - daOverheadGas: 100_000, - gasPerDAByte: 16, - daMultiplier: 10_000, // 1x multiplier - expUSD: execCostUSD, + expUSD: execCostUSD, }, { name: "double native price", @@ -395,10 +435,8 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { SourceTokenData: [][]byte{}, }, }, - daOverheadGas: 100_000, - gasPerDAByte: 0, - daMultiplier: 10_000, // 1x multiplier - expUSD: new(big.Int).Add(execCostUSD, big.NewInt(200_000e9)), + expUSD: new(big.Int).Add(execCostUSD, big.NewInt(200_000e9)), + execEstimatorResponse: []any{int64(100_000), int64(0), int64(10_000), nil}, }, { name: "half multiplier", @@ -411,30 +449,66 @@ func TestDAPriceEstimator_EstimateMsgCostUSD(t *testing.T) { SourceTokenData: [][]byte{}, }, }, - daOverheadGas: 100_000, - gasPerDAByte: 0, - daMultiplier: 5_000, // 0.5x multiplier - expUSD: new(big.Int).Add(execCostUSD, big.NewInt(50_000e9)), + expUSD: new(big.Int).Add(execCostUSD, big.NewInt(50_000e9)), + execEstimatorResponse: []any{int64(100_000), int64(0), int64(5_000), nil}, + }, + { + name: "onRamp reader error", + gasPrice: encodeGasPrice(big.NewInt(1e9), big.NewInt(0)), // 1 gwei DA price, 0 exec price + wrappedNativePrice: big.NewInt(1e18), // $1 + msg: cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta{ + EVM2EVMMessage: cciptypes.EVM2EVMMessage{ + Data: []byte{}, + TokenAmounts: []cciptypes.TokenAmount{}, + SourceTokenData: [][]byte{}, + }, + }, + execEstimatorResponse: []any{int64(0), int64(0), int64(0), errors.New("some reader error")}, + }, + { + name: "execEstimator error", + gasPrice: encodeGasPrice(big.NewInt(1e9), big.NewInt(0)), // 1 gwei DA price, 0 exec price + wrappedNativePrice: big.NewInt(1e18), // $1 + msg: cciptypes.EVM2EVMOnRampCCIPSendRequestedWithMeta{ + EVM2EVMMessage: cciptypes.EVM2EVMMessage{ + Data: []byte{}, + TokenAmounts: []cciptypes.TokenAmount{}, + SourceTokenData: [][]byte{}, + }, + }, + execEstimatorErr: errors.New("some estimator error"), }, } for _, tc := range testCases { - execEstimator := NewMockGasPriceEstimator(t) - execEstimator.On("EstimateMsgCostUSD", mock.Anything, tc.wrappedNativePrice, tc.msg).Return(execCostUSD, nil) - t.Run(tc.name, func(t *testing.T) { + execEstimator := NewMockGasPriceEstimator(t) + execEstimator.On("EstimateMsgCostUSD", mock.Anything, tc.wrappedNativePrice, tc.msg). + Return(execCostUSD, tc.execEstimatorErr) + + feeEstimatorConfig := ccipdatamocks.NewFeeEstimatorConfigReader(t) + if len(tc.execEstimatorResponse) > 0 { + feeEstimatorConfig.On("GetDataAvailabilityConfig", mock.Anything). + Return(tc.execEstimatorResponse...) + } + g := DAGasPriceEstimator{ execEstimator: execEstimator, l1Oracle: nil, priceEncodingLength: daGasPriceEncodingLength, - daOverheadGas: tc.daOverheadGas, - gasPerDAByte: tc.gasPerDAByte, - daMultiplier: tc.daMultiplier, + feeEstimatorConfig: feeEstimatorConfig, } costUSD, err := g.EstimateMsgCostUSD(tc.gasPrice, tc.wrappedNativePrice, tc.msg) - assert.NoError(t, err) - assert.Equal(t, tc.expUSD, costUSD) + + switch { + case len(tc.execEstimatorResponse) == 4 && tc.execEstimatorResponse[3] != nil, + tc.execEstimatorErr != nil: + assert.Error(t, err) + default: + assert.NoError(t, err) + assert.Equal(t, tc.expUSD, costUSD) + } }) } } diff --git a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go index 56e1ddb583..84a6014bef 100644 --- a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go +++ b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator.go @@ -26,7 +26,7 @@ func NewExecGasPriceEstimator(estimator gas.EvmFeeEstimator, maxGasPrice *big.In } func (g ExecGasPriceEstimator) GetGasPrice(ctx context.Context) (*big.Int, error) { - gasPriceWei, _, err := g.estimator.GetFee(ctx, nil, 0, assets.NewWei(g.maxGasPrice)) + gasPriceWei, _, err := g.estimator.GetFee(ctx, nil, 0, assets.NewWei(g.maxGasPrice), nil, nil) if err != nil { return nil, err } diff --git a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go index e1c2fa0398..f9ba1523e5 100644 --- a/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go +++ b/core/services/ocr2/plugins/ccip/prices/exec_price_estimator_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -85,7 +86,7 @@ func TestExecPriceEstimator_GetGasPrice(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { sourceFeeEstimator := mocks.NewEvmFeeEstimator(t) - sourceFeeEstimator.On("GetFee", ctx, []byte(nil), uint64(0), assets.NewWei(tc.maxGasPrice)).Return( + sourceFeeEstimator.On("GetFee", ctx, []byte(nil), uint64(0), assets.NewWei(tc.maxGasPrice), (*common.Address)(nil), (*common.Address)(nil)).Return( tc.sourceFeeEstimatorRespFee, uint64(0), tc.sourceFeeEstimatorRespErr) g := ExecGasPriceEstimator{ diff --git a/core/services/ocr2/plugins/ccip/prices/gas_price_estimator.go b/core/services/ocr2/plugins/ccip/prices/gas_price_estimator.go index 49a6fbcc4a..4aac664e33 100644 --- a/core/services/ocr2/plugins/ccip/prices/gas_price_estimator.go +++ b/core/services/ocr2/plugins/ccip/prices/gas_price_estimator.go @@ -3,6 +3,8 @@ package prices import ( "math/big" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata" + "github.com/Masterminds/semver/v3" "github.com/pkg/errors" @@ -47,12 +49,13 @@ func NewGasPriceEstimatorForCommitPlugin( maxExecGasPrice *big.Int, daDeviationPPB int64, execDeviationPPB int64, + feeEstimatorConfig ccipdata.FeeEstimatorConfigReader, ) (GasPriceEstimatorCommit, error) { switch commitStoreVersion.String() { case "1.0.0", "1.1.0": return NewExecGasPriceEstimator(estimator, maxExecGasPrice, execDeviationPPB), nil case "1.2.0": - return NewDAGasPriceEstimator(estimator, maxExecGasPrice, execDeviationPPB, daDeviationPPB), nil + return NewDAGasPriceEstimator(estimator, maxExecGasPrice, execDeviationPPB, daDeviationPPB, feeEstimatorConfig), nil default: return nil, errors.Errorf("Invalid commitStore version: %s", commitStoreVersion) } diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go index f84a48c1ff..36460683d4 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice.go @@ -45,7 +45,7 @@ func CheckGasPrice(ctx context.Context, upkeepId *big.Int, offchainConfigBytes [ } lggr.Debugf("successfully decode offchain config for %s, max gas price is %s", upkeepId.String(), offchainConfig.MaxGasPrice.String()) - fee, _, err := ge.GetFee(ctx, []byte{}, feeLimit, assets.NewWei(big.NewInt(maxFeePrice))) + fee, _, err := ge.GetFee(ctx, []byte{}, feeLimit, assets.NewWei(big.NewInt(maxFeePrice)), nil, nil) if err != nil { lggr.Errorw("failed to get fee, gas price check is disabled", "upkeepId", upkeepId.String(), "err", err) return encoding.UpkeepFailureReasonNone diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go index 9b5640051d..7b5ef999f3 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/gasprice/gasprice_test.go @@ -86,13 +86,13 @@ func TestGasPrice_Check(t *testing.T) { ctx := testutils.Context(t) ge := gasMocks.NewEvmFeeEstimator(t) if test.FailedToGetFee { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( gas.EvmFee{}, feeLimit, errors.New("failed to retrieve gas price"), ) } else if test.CurrentLegacyGasPrice != nil { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( gas.EvmFee{ Legacy: assets.NewWei(test.CurrentLegacyGasPrice), }, @@ -100,7 +100,7 @@ func TestGasPrice_Check(t *testing.T) { nil, ) } else if test.CurrentDynamicGasPrice != nil { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return( gas.EvmFee{ DynamicFeeCap: assets.NewWei(test.CurrentDynamicGasPrice), DynamicTipCap: assets.NewWei(big.NewInt(1_000_000_000)), diff --git a/core/services/relay/evm/ccip.go b/core/services/relay/evm/ccip.go index 34a732e145..945763de85 100644 --- a/core/services/relay/evm/ccip.go +++ b/core/services/relay/evm/ccip.go @@ -6,18 +6,16 @@ import ( "math/big" "time" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" - + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" "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/logpoller" "github.com/smartcontractkit/chainlink/v2/core/logger" - - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/prices" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" ) var _ cciptypes.CommitStoreReader = (*IncompleteSourceCommitStoreReader)(nil) @@ -26,16 +24,18 @@ var _ cciptypes.CommitStoreReader = (*IncompleteDestCommitStoreReader)(nil) // IncompleteSourceCommitStoreReader is an implementation of CommitStoreReader with the only valid methods being // GasPriceEstimator, ChangeConfig, and OffchainConfig type IncompleteSourceCommitStoreReader struct { - estimator gas.EvmFeeEstimator - gasPriceEstimator *prices.DAGasPriceEstimator - sourceMaxGasPrice *big.Int - offchainConfig cciptypes.CommitOffchainConfig + estimator gas.EvmFeeEstimator + gasPriceEstimator *prices.DAGasPriceEstimator + sourceMaxGasPrice *big.Int + offchainConfig cciptypes.CommitOffchainConfig + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider } -func NewIncompleteSourceCommitStoreReader(estimator gas.EvmFeeEstimator, sourceMaxGasPrice *big.Int) *IncompleteSourceCommitStoreReader { +func NewIncompleteSourceCommitStoreReader(estimator gas.EvmFeeEstimator, sourceMaxGasPrice *big.Int, feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider) *IncompleteSourceCommitStoreReader { return &IncompleteSourceCommitStoreReader{ - estimator: estimator, - sourceMaxGasPrice: sourceMaxGasPrice, + estimator: estimator, + sourceMaxGasPrice: sourceMaxGasPrice, + feeEstimatorConfig: feeEstimatorConfig, } } @@ -55,6 +55,7 @@ func (i *IncompleteSourceCommitStoreReader) ChangeConfig(ctx context.Context, on i.sourceMaxGasPrice, int64(offchainConfigParsed.ExecGasPriceDeviationPPB), int64(offchainConfigParsed.DAGasPriceDeviationPPB), + i.feeEstimatorConfig, ) i.offchainConfig = ccip.NewCommitOffchainConfig( offchainConfigParsed.ExecGasPriceDeviationPPB, @@ -133,8 +134,15 @@ type IncompleteDestCommitStoreReader struct { cs cciptypes.CommitStoreReader } -func NewIncompleteDestCommitStoreReader(lggr logger.Logger, versionFinder ccip.VersionFinder, address cciptypes.Address, ec client.Client, lp logpoller.LogPoller) (*IncompleteDestCommitStoreReader, error) { - cs, err := ccip.NewCommitStoreReader(lggr, versionFinder, address, ec, lp) +func NewIncompleteDestCommitStoreReader( + lggr logger.Logger, + versionFinder ccip.VersionFinder, + address cciptypes.Address, + ec client.Client, + lp logpoller.LogPoller, + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, +) (*IncompleteDestCommitStoreReader, error) { + cs, err := ccip.NewCommitStoreReader(lggr, versionFinder, address, ec, lp, feeEstimatorConfig) if err != nil { return nil, err } diff --git a/core/services/relay/evm/chain_writer.go b/core/services/relay/evm/chain_writer.go index f188ffeced..466811d115 100644 --- a/core/services/relay/evm/chain_writer.go +++ b/core/services/relay/evm/chain_writer.go @@ -186,7 +186,7 @@ func (w *chainWriter) GetFeeComponents(ctx context.Context) (*commontypes.ChainF return nil, fmt.Errorf("gas estimator not available") } - fee, _, err := w.ge.GetFee(ctx, nil, 0, w.maxGasPrice) + fee, _, err := w.ge.GetFee(ctx, nil, 0, w.maxGasPrice, nil, nil) if err != nil { return nil, err } diff --git a/core/services/relay/evm/chain_writer_test.go b/core/services/relay/evm/chain_writer_test.go index 66c85bfc2c..e3fc8f8e22 100644 --- a/core/services/relay/evm/chain_writer_test.go +++ b/core/services/relay/evm/chain_writer_test.go @@ -86,7 +86,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("GetFeeComponents", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: assets.NewWei(big.NewInt(1000000001)), DynamicFeeCap: assets.NewWei(big.NewInt(1000000002)), DynamicTipCap: assets.NewWei(big.NewInt(1000000003)), @@ -112,7 +112,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("Returns Legacy Fee in absence of Dynamic Fee", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: assets.NewWei(big.NewInt(1000000001)), DynamicFeeCap: nil, DynamicTipCap: assets.NewWei(big.NewInt(1000000003)), @@ -124,7 +124,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("Fails when neither legacy or dynamic fee is available", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: nil, DynamicFeeCap: nil, DynamicTipCap: nil, @@ -136,7 +136,7 @@ func TestChainWriter(t *testing.T) { t.Run("Fails when GetFee returns an error", func(t *testing.T) { expectedErr := fmt.Errorf("GetFee error") - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: nil, DynamicFeeCap: nil, DynamicTipCap: nil, @@ -146,7 +146,7 @@ func TestChainWriter(t *testing.T) { }) t.Run("Fails when L1Oracle returns error", func(t *testing.T) { - ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ + ge.On("GetFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(gas.EvmFee{ Legacy: assets.NewWei(big.NewInt(1000000001)), DynamicFeeCap: assets.NewWei(big.NewInt(1000000002)), DynamicTipCap: assets.NewWei(big.NewInt(1000000003)), diff --git a/core/services/relay/evm/commit_provider.go b/core/services/relay/evm/commit_provider.go index 35de8efbd8..780d1cee7b 100644 --- a/core/services/relay/evm/commit_provider.go +++ b/core/services/relay/evm/commit_provider.go @@ -18,18 +18,20 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig" ) var _ commontypes.CCIPCommitProvider = (*SrcCommitProvider)(nil) var _ commontypes.CCIPCommitProvider = (*DstCommitProvider)(nil) type SrcCommitProvider struct { - lggr logger.Logger - startBlock uint64 - client client.Client - lp logpoller.LogPoller - estimator gas.EvmFeeEstimator - maxGasPrice *big.Int + lggr logger.Logger + startBlock uint64 + client client.Client + lp logpoller.LogPoller + estimator gas.EvmFeeEstimator + maxGasPrice *big.Int + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider // these values will be lazily initialized seenOnRampAddress *cciptypes.Address @@ -44,14 +46,16 @@ func NewSrcCommitProvider( lp logpoller.LogPoller, srcEstimator gas.EvmFeeEstimator, maxGasPrice *big.Int, + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, ) commontypes.CCIPCommitProvider { return &SrcCommitProvider{ - lggr: lggr, - startBlock: startBlock, - client: client, - lp: lp, - estimator: srcEstimator, - maxGasPrice: maxGasPrice, + lggr: lggr, + startBlock: startBlock, + client: client, + lp: lp, + estimator: srcEstimator, + maxGasPrice: maxGasPrice, + feeEstimatorConfig: feeEstimatorConfig, } } @@ -65,6 +69,7 @@ type DstCommitProvider struct { configWatcher *configWatcher gasEstimator gas.EvmFeeEstimator maxGasPrice big.Int + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider // these values will be lazily initialized seenCommitStoreAddress *cciptypes.Address @@ -81,6 +86,7 @@ func NewDstCommitProvider( maxGasPrice big.Int, contractTransmitter contractTransmitter, configWatcher *configWatcher, + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, ) commontypes.CCIPCommitProvider { return &DstCommitProvider{ lggr: lggr, @@ -92,6 +98,7 @@ func NewDstCommitProvider( configWatcher: configWatcher, gasEstimator: gasEstimator, maxGasPrice: maxGasPrice, + feeEstimatorConfig: feeEstimatorConfig, } } @@ -170,13 +177,13 @@ func (P *DstCommitProvider) Close() error { if P.seenCommitStoreAddress == nil { return nil } - return ccip.CloseCommitStoreReader(P.lggr, versionFinder, *P.seenCommitStoreAddress, P.client, P.lp) + return ccip.CloseCommitStoreReader(P.lggr, versionFinder, *P.seenCommitStoreAddress, P.client, P.lp, P.feeEstimatorConfig) }) unregisterFuncs = append(unregisterFuncs, func() error { if P.seenOffRampAddress == nil { return nil } - return ccip.CloseOffRampReader(P.lggr, versionFinder, *P.seenOffRampAddress, P.client, P.lp, nil, big.NewInt(0)) + return ccip.CloseOffRampReader(P.lggr, versionFinder, *P.seenOffRampAddress, P.client, P.lp, nil, big.NewInt(0), P.feeEstimatorConfig) }) var multiErr error @@ -241,7 +248,7 @@ func (P *DstCommitProvider) NewPriceGetter(ctx context.Context) (priceGetter cci } func (P *SrcCommitProvider) NewCommitStoreReader(ctx context.Context, commitStoreAddress cciptypes.Address) (commitStoreReader cciptypes.CommitStoreReader, err error) { - commitStoreReader = NewIncompleteSourceCommitStoreReader(P.estimator, P.maxGasPrice) + commitStoreReader = NewIncompleteSourceCommitStoreReader(P.estimator, P.maxGasPrice, P.feeEstimatorConfig) return } @@ -249,7 +256,7 @@ func (P *DstCommitProvider) NewCommitStoreReader(ctx context.Context, commitStor P.seenCommitStoreAddress = &commitStoreAddress versionFinder := ccip.NewEvmVersionFinder() - commitStoreReader, err = NewIncompleteDestCommitStoreReader(P.lggr, versionFinder, commitStoreAddress, P.client, P.lp) + commitStoreReader, err = NewIncompleteDestCommitStoreReader(P.lggr, versionFinder, commitStoreAddress, P.client, P.lp, P.feeEstimatorConfig) return } @@ -259,7 +266,12 @@ func (P *SrcCommitProvider) NewOnRampReader(ctx context.Context, onRampAddress c P.seenDestChainSelector = &destChainSelector versionFinder := ccip.NewEvmVersionFinder() + onRampReader, err = ccip.NewOnRampReader(P.lggr, versionFinder, sourceChainSelector, destChainSelector, onRampAddress, P.lp, P.client) + if err != nil { + return nil, err + } + P.feeEstimatorConfig.SetOnRampReader(onRampReader) return } @@ -272,7 +284,7 @@ func (P *SrcCommitProvider) NewOffRampReader(ctx context.Context, offRampAddr cc } func (P *DstCommitProvider) NewOffRampReader(ctx context.Context, offRampAddr cciptypes.Address) (offRampReader cciptypes.OffRampReader, err error) { - offRampReader, err = ccip.NewOffRampReader(P.lggr, P.versionFinder, offRampAddr, P.client, P.lp, P.gasEstimator, &P.maxGasPrice, true) + offRampReader, err = ccip.NewOffRampReader(P.lggr, P.versionFinder, offRampAddr, P.client, P.lp, P.gasEstimator, &P.maxGasPrice, true, P.feeEstimatorConfig) return } diff --git a/core/services/relay/evm/evm.go b/core/services/relay/evm/evm.go index fe84949903..f2143ceded 100644 --- a/core/services/relay/evm/evm.go +++ b/core/services/relay/evm/evm.go @@ -13,6 +13,8 @@ import ( cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig/interceptors/mantle" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipcommit" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipexec" @@ -45,6 +47,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/keystore" "github.com/smartcontractkit/chainlink/v2/core/services/llo" "github.com/smartcontractkit/chainlink/v2/core/services/llo/bm" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig" lloconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/llo/config" mercuryconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/mercury/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" @@ -406,6 +409,20 @@ func (r *Relayer) NewCCIPCommitProvider(rargs commontypes.RelayArgs, pargs commo sourceStartBlock := commitPluginConfig.SourceStartBlock destStartBlock := commitPluginConfig.DestStartBlock + feeEstimatorConfig := estimatorconfig.NewFeeEstimatorConfigService() + + // CCIPCommit reads only when source chain is Mantle, then reports to dest chain + // to minimize misconfigure risk, might make sense to wire Mantle only when Commit + Mantle + IsSourceProvider + if r.chain.Config().EVM().ChainID().Uint64() == 5003 || r.chain.Config().EVM().ChainID().Uint64() == 5000 { + if commitPluginConfig.IsSourceProvider { + mantleInterceptor, iErr := mantle.NewInterceptor(ctx, r.chain.Client()) + if iErr != nil { + return nil, iErr + } + feeEstimatorConfig.AddGasPriceInterceptor(mantleInterceptor) + } + } + // The src chain implementation of this provider does not need a configWatcher or contractTransmitter; // bail early. if commitPluginConfig.IsSourceProvider { @@ -416,6 +433,7 @@ func (r *Relayer) NewCCIPCommitProvider(rargs commontypes.RelayArgs, pargs commo r.chain.LogPoller(), r.chain.GasEstimator(), r.chain.Config().EVM().GasEstimator().PriceMax().ToInt(), + feeEstimatorConfig, ), nil } @@ -451,6 +469,7 @@ func (r *Relayer) NewCCIPCommitProvider(rargs commontypes.RelayArgs, pargs commo *r.chain.Config().EVM().GasEstimator().PriceMax().ToInt(), *contractTransmitter, configWatcher, + feeEstimatorConfig, ), nil } @@ -473,6 +492,20 @@ func (r *Relayer) NewCCIPExecProvider(rargs commontypes.RelayArgs, pargs commont usdcConfig := execPluginConfig.USDCConfig + feeEstimatorConfig := estimatorconfig.NewFeeEstimatorConfigService() + + // CCIPExec reads when dest chain is mantle, and uses it to calc boosting in batching + // to minimize misconfigure risk, make sense to wire Mantle only when Exec + Mantle + !IsSourceProvider + if r.chain.Config().EVM().ChainID().Uint64() == 5003 || r.chain.Config().EVM().ChainID().Uint64() == 5000 { + if !execPluginConfig.IsSourceProvider { + mantleInterceptor, iErr := mantle.NewInterceptor(ctx, r.chain.Client()) + if iErr != nil { + return nil, iErr + } + feeEstimatorConfig.AddGasPriceInterceptor(mantleInterceptor) + } + } + // The src chain implementation of this provider does not need a configWatcher or contractTransmitter; // bail early. if execPluginConfig.IsSourceProvider { @@ -489,6 +522,7 @@ func (r *Relayer) NewCCIPExecProvider(rargs commontypes.RelayArgs, pargs commont int(usdcConfig.AttestationAPITimeoutSeconds), usdcConfig.AttestationAPIIntervalMilliseconds, usdcConfig.SourceMessageTransmitterAddress, + feeEstimatorConfig, ) } @@ -524,6 +558,7 @@ func (r *Relayer) NewCCIPExecProvider(rargs commontypes.RelayArgs, pargs commont configWatcher, r.chain.GasEstimator(), *r.chain.Config().EVM().GasEstimator().PriceMax().ToInt(), + feeEstimatorConfig, r.chain.TxManager(), cciptypes.Address(rargs.ContractID), ) diff --git a/core/services/relay/evm/exec_provider.go b/core/services/relay/evm/exec_provider.go index d0e567af73..e50ae41351 100644 --- a/core/services/relay/evm/exec_provider.go +++ b/core/services/relay/evm/exec_provider.go @@ -23,6 +23,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/estimatorconfig" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/tokendata/usdc" ) @@ -40,6 +41,8 @@ type SrcExecProvider struct { usdcAttestationAPIIntervalMilliseconds int usdcSrcMsgTransmitterAddr common.Address + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider + // these values are nil and are updated for Close() seenOnRampAddress *cciptypes.Address seenSourceChainSelector *uint64 @@ -59,6 +62,7 @@ func NewSrcExecProvider( usdcAttestationAPITimeoutSeconds int, usdcAttestationAPIIntervalMilliseconds int, usdcSrcMsgTransmitterAddr common.Address, + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, ) (commontypes.CCIPExecProvider, error) { var usdcReader *ccip.USDCReaderImpl var err error @@ -82,6 +86,7 @@ func NewSrcExecProvider( usdcAttestationAPITimeoutSeconds: usdcAttestationAPITimeoutSeconds, usdcAttestationAPIIntervalMilliseconds: usdcAttestationAPIIntervalMilliseconds, usdcSrcMsgTransmitterAddr: usdcSrcMsgTransmitterAddr, + feeEstimatorConfig: feeEstimatorConfig, }, nil } @@ -163,7 +168,7 @@ func (s *SrcExecProvider) GetTransactionStatus(ctx context.Context, transactionI } func (s *SrcExecProvider) NewCommitStoreReader(ctx context.Context, addr cciptypes.Address) (commitStoreReader cciptypes.CommitStoreReader, err error) { - commitStoreReader = NewIncompleteSourceCommitStoreReader(s.estimator, s.maxGasPrice) + commitStoreReader = NewIncompleteSourceCommitStoreReader(s.estimator, s.maxGasPrice, s.feeEstimatorConfig) return } @@ -176,6 +181,10 @@ func (s *SrcExecProvider) NewOnRampReader(ctx context.Context, onRampAddress cci versionFinder := ccip.NewEvmVersionFinder() onRampReader, err = ccip.NewOnRampReader(s.lggr, versionFinder, sourceChainSelector, destChainSelector, onRampAddress, s.lp, s.client) + if err != nil { + return nil, err + } + s.feeEstimatorConfig.SetOnRampReader(onRampReader) return } @@ -236,6 +245,7 @@ type DstExecProvider struct { configWatcher *configWatcher gasEstimator gas.EvmFeeEstimator maxGasPrice big.Int + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider txm txmgr.TxManager offRampAddress cciptypes.Address @@ -253,6 +263,7 @@ func NewDstExecProvider( configWatcher *configWatcher, gasEstimator gas.EvmFeeEstimator, maxGasPrice big.Int, + feeEstimatorConfig estimatorconfig.FeeEstimatorConfigProvider, txm txmgr.TxManager, offRampAddress cciptypes.Address, ) (commontypes.CCIPExecProvider, error) { @@ -266,6 +277,7 @@ func NewDstExecProvider( configWatcher: configWatcher, gasEstimator: gasEstimator, maxGasPrice: maxGasPrice, + feeEstimatorConfig: feeEstimatorConfig, txm: txm, offRampAddress: offRampAddress, }, nil @@ -295,10 +307,10 @@ func (d *DstExecProvider) Close() error { if d.seenCommitStoreAddr == nil { return nil } - return ccip.CloseCommitStoreReader(d.lggr, versionFinder, *d.seenCommitStoreAddr, d.client, d.lp) + return ccip.CloseCommitStoreReader(d.lggr, versionFinder, *d.seenCommitStoreAddr, d.client, d.lp, d.feeEstimatorConfig) }) unregisterFuncs = append(unregisterFuncs, func() error { - return ccip.CloseOffRampReader(d.lggr, versionFinder, d.offRampAddress, d.client, d.lp, nil, big.NewInt(0)) + return ccip.CloseOffRampReader(d.lggr, versionFinder, d.offRampAddress, d.client, d.lp, nil, big.NewInt(0), d.feeEstimatorConfig) }) var multiErr error @@ -346,12 +358,12 @@ func (d *DstExecProvider) NewCommitStoreReader(ctx context.Context, addr cciptyp d.seenCommitStoreAddr = &addr versionFinder := ccip.NewEvmVersionFinder() - commitStoreReader, err = NewIncompleteDestCommitStoreReader(d.lggr, versionFinder, addr, d.client, d.lp) + commitStoreReader, err = NewIncompleteDestCommitStoreReader(d.lggr, versionFinder, addr, d.client, d.lp, d.feeEstimatorConfig) return } func (d *DstExecProvider) NewOffRampReader(ctx context.Context, offRampAddress cciptypes.Address) (offRampReader cciptypes.OffRampReader, err error) { - offRampReader, err = ccip.NewOffRampReader(d.lggr, d.versionFinder, offRampAddress, d.client, d.lp, d.gasEstimator, &d.maxGasPrice, true) + offRampReader, err = ccip.NewOffRampReader(d.lggr, d.versionFinder, offRampAddress, d.client, d.lp, d.gasEstimator, &d.maxGasPrice, true, d.feeEstimatorConfig) return } diff --git a/core/services/relay/evm/interceptors/mantle/interceptor.go b/core/services/relay/evm/interceptors/mantle/interceptor.go new file mode 100644 index 0000000000..8d5d3bb6c2 --- /dev/null +++ b/core/services/relay/evm/interceptors/mantle/interceptor.go @@ -0,0 +1,80 @@ +package mantle + +import ( + "context" + "fmt" + "math/big" + "strings" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + evmClient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas/rollups" +) + +const ( + // tokenRatio is not volatile and can be requested not often. + tokenRatioUpdateInterval = 60 * time.Minute + // tokenRatio fetches the tokenRatio used for Mantle's gas price calculation + tokenRatioMethod = "tokenRatio" + mantleTokenRatioAbiString = `[{"inputs":[],"name":"tokenRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` +) + +type Interceptor struct { + client evmClient.Client + tokenRatioCallData []byte + tokenRatio *big.Int + tokenRatioLastUpdate time.Time +} + +func NewInterceptor(_ context.Context, client evmClient.Client) (*Interceptor, error) { + // Encode calldata for tokenRatio method + tokenRatioMethodAbi, err := abi.JSON(strings.NewReader(mantleTokenRatioAbiString)) + if err != nil { + return nil, fmt.Errorf("failed to parse GasPriceOracle %s() method ABI for Mantle; %v", tokenRatioMethod, err) + } + tokenRatioCallData, err := tokenRatioMethodAbi.Pack(tokenRatioMethod) + if err != nil { + return nil, fmt.Errorf("failed to parse GasPriceOracle %s() calldata for Mantle; %v", tokenRatioMethod, err) + } + + return &Interceptor{ + client: client, + tokenRatioCallData: tokenRatioCallData, + }, nil +} + +// ModifyGasPriceComponents returns modified gasPrice. +func (i *Interceptor) ModifyGasPriceComponents(ctx context.Context, execGasPrice, daGasPrice *big.Int) (*big.Int, *big.Int, error) { + if time.Since(i.tokenRatioLastUpdate) > tokenRatioUpdateInterval { + mantleTokenRatio, err := i.getMantleTokenRatio(ctx) + if err != nil { + return nil, nil, err + } + + i.tokenRatio, i.tokenRatioLastUpdate = mantleTokenRatio, time.Now() + } + + // multiply daGasPrice and execGas price by tokenRatio + newExecGasPrice := new(big.Int).Mul(execGasPrice, i.tokenRatio) + newDAGasPrice := new(big.Int).Mul(daGasPrice, i.tokenRatio) + return newExecGasPrice, newDAGasPrice, nil +} + +// getMantleTokenRatio Requests and returns a token ratio value for the Mantle chain. +func (i *Interceptor) getMantleTokenRatio(ctx context.Context) (*big.Int, error) { + precompile := common.HexToAddress(rollups.OPGasOracleAddress) + tokenRatio, err := i.client.CallContract(ctx, ethereum.CallMsg{ + To: &precompile, + Data: i.tokenRatioCallData, + }, nil) + + if err != nil { + return nil, fmt.Errorf("getMantleTokenRatio call failed: %w", err) + } + + return new(big.Int).SetBytes(tokenRatio), nil +} diff --git a/core/services/relay/evm/interceptors/mantle/interceptor_test.go b/core/services/relay/evm/interceptors/mantle/interceptor_test.go new file mode 100644 index 0000000000..9134d996c2 --- /dev/null +++ b/core/services/relay/evm/interceptors/mantle/interceptor_test.go @@ -0,0 +1,96 @@ +package mantle + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" +) + +func TestInterceptor(t *testing.T) { + ethClient := mocks.NewClient(t) + ctx := context.Background() + + tokenRatio := big.NewInt(10) + interceptor, err := NewInterceptor(ctx, ethClient) + require.NoError(t, err) + + // request token ratio + ethClient.On("CallContract", ctx, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})). + Return(common.BigToHash(tokenRatio).Bytes(), nil).Once() + + modExecGasPrice, modDAGasPrice, err := interceptor.ModifyGasPriceComponents(ctx, big.NewInt(1), big.NewInt(1)) + require.NoError(t, err) + require.Equal(t, int64(10), modExecGasPrice.Int64()) + require.Equal(t, int64(10), modDAGasPrice.Int64()) + + // second call won't invoke eth client + modExecGasPrice, modDAGasPrice, err = interceptor.ModifyGasPriceComponents(ctx, big.NewInt(2), big.NewInt(1)) + require.NoError(t, err) + require.Equal(t, int64(20), modExecGasPrice.Int64()) + require.Equal(t, int64(10), modDAGasPrice.Int64()) +} + +func TestModifyGasPriceComponents(t *testing.T) { + testCases := map[string]struct { + execGasPrice *big.Int + daGasPrice *big.Int + tokenRatio *big.Int + resultExecGasPrice *big.Int + resultDAGasPrice *big.Int + }{ + "regular": { + execGasPrice: big.NewInt(1000), + daGasPrice: big.NewInt(100), + resultExecGasPrice: big.NewInt(2000), + resultDAGasPrice: big.NewInt(200), + tokenRatio: big.NewInt(2), + }, + "zero DAGasPrice": { + execGasPrice: big.NewInt(1000), + daGasPrice: big.NewInt(0), + resultExecGasPrice: big.NewInt(5000), + resultDAGasPrice: big.NewInt(0), + tokenRatio: big.NewInt(5), + }, + "zero ExecGasPrice": { + execGasPrice: big.NewInt(0), + daGasPrice: big.NewInt(10), + resultExecGasPrice: big.NewInt(0), + resultDAGasPrice: big.NewInt(50), + tokenRatio: big.NewInt(5), + }, + "zero token ratio": { + execGasPrice: big.NewInt(15), + daGasPrice: big.NewInt(10), + resultExecGasPrice: big.NewInt(0), + resultDAGasPrice: big.NewInt(0), + tokenRatio: big.NewInt(0), + }, + } + + for tcName, tc := range testCases { + t.Run(tcName, func(t *testing.T) { + ethClient := mocks.NewClient(t) + ctx := context.Background() + + interceptor, err := NewInterceptor(ctx, ethClient) + require.NoError(t, err) + + // request token ratio + ethClient.On("CallContract", ctx, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})). + Return(common.BigToHash(tc.tokenRatio).Bytes(), nil).Once() + + modExecGasPrice, modDAGasPrice, err := interceptor.ModifyGasPriceComponents(ctx, tc.execGasPrice, tc.daGasPrice) + require.NoError(t, err) + require.Equal(t, tc.resultExecGasPrice.Int64(), modExecGasPrice.Int64()) + require.Equal(t, tc.resultDAGasPrice.Int64(), modDAGasPrice.Int64()) + }) + } +} diff --git a/core/services/synchronization/common.go b/core/services/synchronization/common.go index 394830a76a..a6c0191e3a 100644 --- a/core/services/synchronization/common.go +++ b/core/services/synchronization/common.go @@ -28,6 +28,7 @@ const ( OCR3CCIPCommit TelemetryType = "ocr3-ccip-commit" OCR3CCIPExec TelemetryType = "ocr3-ccip-exec" OCR3CCIPBootstrap TelemetryType = "ocr3-bootstrap" + HeadReport TelemetryType = "head-report" ) type TelemPayload struct { diff --git a/core/services/synchronization/telem/telem_head_report.pb.go b/core/services/synchronization/telem/telem_head_report.pb.go new file mode 100644 index 0000000000..12801314a7 --- /dev/null +++ b/core/services/synchronization/telem/telem_head_report.pb.go @@ -0,0 +1,256 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc v4.25.1 +// source: core/services/synchronization/telem/telem_head_report.proto + +package telem + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type HeadReportRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ChainID string `protobuf:"bytes,1,opt,name=chainID,proto3" json:"chainID,omitempty"` + Latest *Block `protobuf:"bytes,2,opt,name=latest,proto3" json:"latest,omitempty"` + Finalized *Block `protobuf:"bytes,3,opt,name=finalized,proto3,oneof" json:"finalized,omitempty"` +} + +func (x *HeadReportRequest) Reset() { + *x = HeadReportRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_core_services_synchronization_telem_telem_head_report_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HeadReportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeadReportRequest) ProtoMessage() {} + +func (x *HeadReportRequest) ProtoReflect() protoreflect.Message { + mi := &file_core_services_synchronization_telem_telem_head_report_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeadReportRequest.ProtoReflect.Descriptor instead. +func (*HeadReportRequest) Descriptor() ([]byte, []int) { + return file_core_services_synchronization_telem_telem_head_report_proto_rawDescGZIP(), []int{0} +} + +func (x *HeadReportRequest) GetChainID() string { + if x != nil { + return x.ChainID + } + return "" +} + +func (x *HeadReportRequest) GetLatest() *Block { + if x != nil { + return x.Latest + } + return nil +} + +func (x *HeadReportRequest) GetFinalized() *Block { + if x != nil { + return x.Finalized + } + return nil +} + +type Block struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp uint64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Number uint64 `protobuf:"varint,2,opt,name=number,proto3" json:"number,omitempty"` + Hash string `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"` +} + +func (x *Block) Reset() { + *x = Block{} + if protoimpl.UnsafeEnabled { + mi := &file_core_services_synchronization_telem_telem_head_report_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Block) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Block) ProtoMessage() {} + +func (x *Block) ProtoReflect() protoreflect.Message { + mi := &file_core_services_synchronization_telem_telem_head_report_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Block.ProtoReflect.Descriptor instead. +func (*Block) Descriptor() ([]byte, []int) { + return file_core_services_synchronization_telem_telem_head_report_proto_rawDescGZIP(), []int{1} +} + +func (x *Block) GetTimestamp() uint64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *Block) GetNumber() uint64 { + if x != nil { + return x.Number + } + return 0 +} + +func (x *Block) GetHash() string { + if x != nil { + return x.Hash + } + return "" +} + +var File_core_services_synchronization_telem_telem_head_report_proto protoreflect.FileDescriptor + +var file_core_services_synchronization_telem_telem_head_report_proto_rawDesc = []byte{ + 0x0a, 0x3b, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, + 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, + 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x5f, 0x68, 0x65, 0x61, 0x64, + 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x74, + 0x65, 0x6c, 0x65, 0x6d, 0x22, 0x92, 0x01, 0x0a, 0x11, 0x48, 0x65, 0x61, 0x64, 0x52, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x49, 0x44, 0x12, 0x24, 0x0a, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x2e, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x52, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x09, 0x66, 0x69, + 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, + 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x09, 0x66, + 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, + 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x22, 0x51, 0x0a, 0x05, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x42, 0x4e, 0x5a, 0x4c, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6d, 0x61, 0x72, 0x74, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x6b, 0x69, 0x74, 0x2f, 0x63, 0x68, 0x61, 0x69, + 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_core_services_synchronization_telem_telem_head_report_proto_rawDescOnce sync.Once + file_core_services_synchronization_telem_telem_head_report_proto_rawDescData = file_core_services_synchronization_telem_telem_head_report_proto_rawDesc +) + +func file_core_services_synchronization_telem_telem_head_report_proto_rawDescGZIP() []byte { + file_core_services_synchronization_telem_telem_head_report_proto_rawDescOnce.Do(func() { + file_core_services_synchronization_telem_telem_head_report_proto_rawDescData = protoimpl.X.CompressGZIP(file_core_services_synchronization_telem_telem_head_report_proto_rawDescData) + }) + return file_core_services_synchronization_telem_telem_head_report_proto_rawDescData +} + +var file_core_services_synchronization_telem_telem_head_report_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_core_services_synchronization_telem_telem_head_report_proto_goTypes = []interface{}{ + (*HeadReportRequest)(nil), // 0: telem.HeadReportRequest + (*Block)(nil), // 1: telem.Block +} +var file_core_services_synchronization_telem_telem_head_report_proto_depIdxs = []int32{ + 1, // 0: telem.HeadReportRequest.latest:type_name -> telem.Block + 1, // 1: telem.HeadReportRequest.finalized:type_name -> telem.Block + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_core_services_synchronization_telem_telem_head_report_proto_init() } +func file_core_services_synchronization_telem_telem_head_report_proto_init() { + if File_core_services_synchronization_telem_telem_head_report_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_core_services_synchronization_telem_telem_head_report_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HeadReportRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_core_services_synchronization_telem_telem_head_report_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Block); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_core_services_synchronization_telem_telem_head_report_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_core_services_synchronization_telem_telem_head_report_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_core_services_synchronization_telem_telem_head_report_proto_goTypes, + DependencyIndexes: file_core_services_synchronization_telem_telem_head_report_proto_depIdxs, + MessageInfos: file_core_services_synchronization_telem_telem_head_report_proto_msgTypes, + }.Build() + File_core_services_synchronization_telem_telem_head_report_proto = out.File + file_core_services_synchronization_telem_telem_head_report_proto_rawDesc = nil + file_core_services_synchronization_telem_telem_head_report_proto_goTypes = nil + file_core_services_synchronization_telem_telem_head_report_proto_depIdxs = nil +} diff --git a/core/services/synchronization/telem/telem_head_report.proto b/core/services/synchronization/telem/telem_head_report.proto new file mode 100644 index 0000000000..6f4cf2ddae --- /dev/null +++ b/core/services/synchronization/telem/telem_head_report.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option go_package = "github.com/smartcontractkit/chainlink/v2/core/services/synchronization/telem"; + +package telem; + +message HeadReportRequest { + string chainID = 1; + Block latest = 2; + optional Block finalized = 3; +} + +message Block { + uint64 timestamp = 1; + uint64 number = 2; + string hash = 3; +} diff --git a/core/services/telemetry/monitoring_endpoint_generator_mock.go b/core/services/telemetry/monitoring_endpoint_generator_mock.go new file mode 100644 index 0000000000..a0fc503ecc --- /dev/null +++ b/core/services/telemetry/monitoring_endpoint_generator_mock.go @@ -0,0 +1,88 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package telemetry + +import ( + commontypes "github.com/smartcontractkit/libocr/commontypes" + mock "github.com/stretchr/testify/mock" + + synchronization "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" +) + +// MockMonitoringEndpointGenerator is an autogenerated mock type for the MonitoringEndpointGenerator type +type MockMonitoringEndpointGenerator struct { + mock.Mock +} + +type MockMonitoringEndpointGenerator_Expecter struct { + mock *mock.Mock +} + +func (_m *MockMonitoringEndpointGenerator) EXPECT() *MockMonitoringEndpointGenerator_Expecter { + return &MockMonitoringEndpointGenerator_Expecter{mock: &_m.Mock} +} + +// GenMonitoringEndpoint provides a mock function with given fields: network, chainID, contractID, telemType +func (_m *MockMonitoringEndpointGenerator) GenMonitoringEndpoint(network string, chainID string, contractID string, telemType synchronization.TelemetryType) commontypes.MonitoringEndpoint { + ret := _m.Called(network, chainID, contractID, telemType) + + if len(ret) == 0 { + panic("no return value specified for GenMonitoringEndpoint") + } + + var r0 commontypes.MonitoringEndpoint + if rf, ok := ret.Get(0).(func(string, string, string, synchronization.TelemetryType) commontypes.MonitoringEndpoint); ok { + r0 = rf(network, chainID, contractID, telemType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(commontypes.MonitoringEndpoint) + } + } + + return r0 +} + +// MockMonitoringEndpointGenerator_GenMonitoringEndpoint_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenMonitoringEndpoint' +type MockMonitoringEndpointGenerator_GenMonitoringEndpoint_Call struct { + *mock.Call +} + +// GenMonitoringEndpoint is a helper method to define mock.On call +// - network string +// - chainID string +// - contractID string +// - telemType synchronization.TelemetryType +func (_e *MockMonitoringEndpointGenerator_Expecter) GenMonitoringEndpoint(network interface{}, chainID interface{}, contractID interface{}, telemType interface{}) *MockMonitoringEndpointGenerator_GenMonitoringEndpoint_Call { + return &MockMonitoringEndpointGenerator_GenMonitoringEndpoint_Call{Call: _e.mock.On("GenMonitoringEndpoint", network, chainID, contractID, telemType)} +} + +func (_c *MockMonitoringEndpointGenerator_GenMonitoringEndpoint_Call) Run(run func(network string, chainID string, contractID string, telemType synchronization.TelemetryType)) *MockMonitoringEndpointGenerator_GenMonitoringEndpoint_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(string), args[3].(synchronization.TelemetryType)) + }) + return _c +} + +func (_c *MockMonitoringEndpointGenerator_GenMonitoringEndpoint_Call) Return(_a0 commontypes.MonitoringEndpoint) *MockMonitoringEndpointGenerator_GenMonitoringEndpoint_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockMonitoringEndpointGenerator_GenMonitoringEndpoint_Call) RunAndReturn(run func(string, string, string, synchronization.TelemetryType) commontypes.MonitoringEndpoint) *MockMonitoringEndpointGenerator_GenMonitoringEndpoint_Call { + _c.Call.Return(run) + return _c +} + +// NewMockMonitoringEndpointGenerator creates a new instance of MockMonitoringEndpointGenerator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockMonitoringEndpointGenerator(t interface { + mock.TestingT + Cleanup(func()) +}) *MockMonitoringEndpointGenerator { + mock := &MockMonitoringEndpointGenerator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/web/evm_transfer_controller.go b/core/web/evm_transfer_controller.go index 88d3dead4c..75f6c07b6d 100644 --- a/core/web/evm_transfer_controller.go +++ b/core/web/evm_transfer_controller.go @@ -54,7 +54,7 @@ func (tc *EVMTransfersController) Create(c *gin.Context) { } if !tr.AllowHigherAmounts { - err = ValidateEthBalanceForTransfer(c, chain, tr.FromAddress, tr.Amount) + err = ValidateEthBalanceForTransfer(c, chain, tr.FromAddress, tr.Amount, tr.DestinationAddress) if err != nil { jsonAPIError(c, http.StatusUnprocessableEntity, errors.Errorf("transaction failed: %v", err)) return @@ -92,7 +92,7 @@ func (tc *EVMTransfersController) Create(c *gin.Context) { } // ValidateEthBalanceForTransfer validates that the current balance can cover the transaction amount -func ValidateEthBalanceForTransfer(c *gin.Context, chain legacyevm.Chain, fromAddr common.Address, amount assets.Eth) error { +func ValidateEthBalanceForTransfer(c *gin.Context, chain legacyevm.Chain, fromAddr common.Address, amount assets.Eth, toAddr common.Address) error { var err error var balance *big.Int @@ -116,7 +116,7 @@ func ValidateEthBalanceForTransfer(c *gin.Context, chain legacyevm.Chain, fromAd gasLimit := chain.Config().EVM().GasEstimator().LimitTransfer() estimator := chain.GasEstimator() - amountWithFees, err := estimator.GetMaxCost(c, amount, nil, gasLimit, chain.Config().EVM().GasEstimator().PriceMaxKey(fromAddr)) + amountWithFees, err := estimator.GetMaxCost(c, amount, nil, gasLimit, chain.Config().EVM().GasEstimator().PriceMaxKey(fromAddr), &fromAddr, &toAddr) if err != nil { return err } diff --git a/core/web/resolver/testdata/config-full.toml b/core/web/resolver/testdata/config-full.toml index c5547f8698..e955ad4f29 100644 --- a/core/web/resolver/testdata/config-full.toml +++ b/core/web/resolver/testdata/config-full.toml @@ -288,6 +288,7 @@ MinContractPayment = '9.223372036854775807 link' NonceAutoSync = true NoNewHeadsThreshold = '1m0s' OperatorFactoryAddress = '0xa5B85635Be42F21f94F28034B7DA440EeFF0F418' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 17 RPCBlockQueryDelay = 10 FinalizedBlockOffset = 0 @@ -316,6 +317,7 @@ LimitDefault = 12 LimitMax = 17 LimitMultiplier = '1.234' LimitTransfer = 100 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 10 BumpThreshold = 6 @@ -340,6 +342,9 @@ CheckInclusionPercentile = 19 EIP1559FeeCapBufferBlocks = 13 TransactionPercentile = 15 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '1s' + [EVM.HeadTracker] HistoryDepth = 15 MaxBufferSize = 17 @@ -363,6 +368,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.NodePool.Errors] NonceTooLow = '(: |^)nonce too low' diff --git a/core/web/resolver/testdata/config-multi-chain-effective.toml b/core/web/resolver/testdata/config-multi-chain-effective.toml index 8c063d0e9e..b6358ef66d 100644 --- a/core/web/resolver/testdata/config-multi-chain-effective.toml +++ b/core/web/resolver/testdata/config-multi-chain-effective.toml @@ -275,6 +275,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x3E64Cd889482443324F91bFA9c84fE72A511f48A' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -303,6 +304,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -318,6 +320,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -335,6 +340,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 @@ -375,6 +381,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x8007e24251b1D2Fc518Eb843A701d9cD21fe0aA3' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -403,6 +410,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -418,6 +426,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -435,6 +446,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 @@ -469,6 +481,7 @@ MinIncomingConfirmations = 5 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 100 RPCBlockQueryDelay = 10 FinalizedBlockOffset = 0 @@ -497,6 +510,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -512,6 +526,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -529,6 +546,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/core/web/testdata/body/health.html b/core/web/testdata/body/health.html index 2a1b222753..4e244a9fe7 100644 --- a/core/web/testdata/body/health.html +++ b/core/web/testdata/body/health.html @@ -69,6 +69,9 @@ +
+ HeadReporter +
JobSpawner
@@ -96,9 +99,6 @@ BridgeCache -
- PromReporter -
TelemetryManager
diff --git a/core/web/testdata/body/health.json b/core/web/testdata/body/health.json index 10415c0abd..224a3534c9 100644 --- a/core/web/testdata/body/health.json +++ b/core/web/testdata/body/health.json @@ -99,6 +99,15 @@ "output": "" } }, + { + "type": "checks", + "id": "HeadReporter", + "attributes": { + "name": "HeadReporter", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "JobSpawner", @@ -162,15 +171,6 @@ "output": "" } }, - { - "type": "checks", - "id": "PromReporter", - "attributes": { - "name": "PromReporter", - "status": "passing", - "output": "" - } - }, { "type": "checks", "id": "TelemetryManager", diff --git a/core/web/testdata/body/health.txt b/core/web/testdata/body/health.txt index 09c8cff6c2..0fbac846a6 100644 --- a/core/web/testdata/body/health.txt +++ b/core/web/testdata/body/health.txt @@ -10,6 +10,7 @@ ok EVM.0.Txm.BlockHistoryEstimator ok EVM.0.Txm.Broadcaster ok EVM.0.Txm.Confirmer ok EVM.0.Txm.WrappedEvmEstimator +ok HeadReporter ok JobSpawner ok Mailbox.Monitor ok Mercury.WSRPCPool @@ -17,5 +18,4 @@ ok Mercury.WSRPCPool.CacheSet ok PipelineORM ok PipelineRunner ok PipelineRunner.BridgeCache -ok PromReporter ok TelemetryManager diff --git a/docs/CONFIG.md b/docs/CONFIG.md index e18abcf811..e569b76190 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -1790,6 +1790,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x3E64Cd889482443324F91bFA9c84fE72A511f48A' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -1818,6 +1819,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -1833,6 +1835,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -1850,6 +1855,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -1884,6 +1890,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -1912,6 +1919,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -1927,6 +1935,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -1944,6 +1955,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -1978,6 +1990,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -2006,6 +2019,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2021,6 +2035,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2038,6 +2055,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2072,6 +2090,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -2100,6 +2119,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2115,6 +2135,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2132,6 +2155,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2167,6 +2191,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -2195,6 +2220,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -2210,6 +2236,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -2227,6 +2256,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -2261,6 +2291,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -2289,6 +2320,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2304,6 +2336,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2321,6 +2356,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2355,6 +2391,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -2383,6 +2420,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2398,6 +2436,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2415,6 +2456,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2450,6 +2492,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x8007e24251b1D2Fc518Eb843A701d9cD21fe0aA3' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -2478,6 +2521,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2493,6 +2537,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2510,6 +2557,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2544,6 +2592,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 @@ -2572,6 +2621,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -2587,6 +2637,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2604,6 +2657,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2637,6 +2691,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -2665,6 +2720,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2680,6 +2736,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2697,6 +2756,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2730,6 +2790,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -2758,6 +2819,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2773,6 +2835,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2790,6 +2855,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2823,6 +2889,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -2851,6 +2918,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -2866,6 +2934,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2883,6 +2954,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -2917,6 +2989,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 @@ -2945,6 +3018,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -2960,6 +3034,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -2977,6 +3054,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3012,6 +3090,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -3040,6 +3119,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3055,6 +3135,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3072,6 +3155,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3106,6 +3190,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 @@ -3134,6 +3219,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -3149,6 +3235,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3166,6 +3255,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3200,6 +3290,7 @@ MinIncomingConfirmations = 5 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 100 RPCBlockQueryDelay = 10 FinalizedBlockOffset = 0 @@ -3228,6 +3319,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -3243,6 +3335,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -3260,6 +3355,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3294,6 +3390,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '12m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 100 RPCBlockQueryDelay = 15 FinalizedBlockOffset = 0 @@ -3322,6 +3419,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 mwei' BumpPercent = 40 BumpThreshold = 3 @@ -3337,6 +3435,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -3354,6 +3455,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -3388,6 +3490,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '6m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 100 RPCBlockQueryDelay = 15 FinalizedBlockOffset = 0 @@ -3416,6 +3519,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 mwei' BumpPercent = 40 BumpThreshold = 3 @@ -3431,6 +3535,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -3448,6 +3555,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -3464,6 +3572,106 @@ GasLimit = 5400000

+
Bsquared Mainnet (223)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'optimismBedrock' +FinalityDepth = 2000 +FinalityTagEnabled = true +LogBackfillBatchSize = 1000 +LogPollInterval = '5s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 10000 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 3 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '1h10m0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' + +[Transactions.AutoPurge] +Enabled = false + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'FeeHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 gwei' +LimitDefault = 8000000 +LimitMax = 8000000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 100 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '2s' + +[HeadTracker] +HistoryDepth = 100 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5400000 +``` + +

+
Fantom Mainnet (250)

```toml @@ -3482,6 +3690,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 @@ -3510,6 +3719,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3525,6 +3735,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -3542,6 +3755,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3576,6 +3790,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -3604,6 +3819,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -3619,6 +3835,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 400 MaxBufferSize = 3 @@ -3636,6 +3855,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -3670,6 +3890,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '1m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -3698,6 +3919,7 @@ LimitDefault = 100000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3713,6 +3935,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -3730,6 +3955,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3764,6 +3990,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '1m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -3792,6 +4019,7 @@ LimitDefault = 2500000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3807,6 +4035,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 250 MaxBufferSize = 3 @@ -3824,6 +4055,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3858,6 +4090,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '1m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -3886,6 +4119,7 @@ LimitDefault = 2500000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -3901,6 +4135,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 1500 MaxBufferSize = 3 @@ -3918,6 +4155,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -3953,6 +4191,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -3981,6 +4220,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -3996,6 +4236,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -4013,6 +4256,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4029,6 +4273,106 @@ GasLimit = 6500000

+
Worldchain Mainnet (480)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'optimismBedrock' +FinalityDepth = 2500 +FinalityTagEnabled = true +LogBackfillBatchSize = 1000 +LogPollInterval = '5s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 10000 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 3 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '1h30m0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' + +[Transactions.AutoPurge] +Enabled = false + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'FeeHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 gwei' +LimitDefault = 8000000 +LimitMax = 8000000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 100 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '2s' + +[HeadTracker] +HistoryDepth = 100 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5400000 +``` + +

+
Metis Rinkeby (588)

```toml @@ -4047,6 +4391,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -4075,6 +4420,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4090,6 +4436,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4107,6 +4456,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4129,8 +4479,9 @@ GasLimit = 5400000 AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false +ChainType = 'astar' FinalityDepth = 100 -FinalityTagEnabled = false +FinalityTagEnabled = true LogBackfillBatchSize = 1000 LogPollInterval = '6s' LogKeepBlocksDepth = 100000 @@ -4140,6 +4491,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -4168,6 +4520,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4183,6 +4536,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4200,6 +4556,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -4234,6 +4591,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -4262,6 +4620,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 60 @@ -4278,6 +4637,9 @@ CheckInclusionPercentile = 90 EIP1559FeeCapBufferBlocks = 0 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -4295,6 +4657,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -4328,6 +4691,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -4356,6 +4720,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -4371,6 +4736,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4388,6 +4756,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4410,7 +4779,7 @@ GasLimit = 5400000 AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -ChainType = 'metis' +ChainType = 'optimismBedrock' FinalityDepth = 10 FinalityTagEnabled = true LogBackfillBatchSize = 1000 @@ -4422,6 +4791,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -4450,6 +4820,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4465,6 +4836,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4482,6 +4856,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4516,6 +4891,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '6m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 100 RPCBlockQueryDelay = 15 FinalizedBlockOffset = 0 @@ -4536,15 +4912,16 @@ Enabled = false Enabled = true [GasEstimator] -Mode = 'BlockHistory' +Mode = 'FeeHistory' PriceDefault = '20 gwei' PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' -PriceMin = '100 mwei' +PriceMin = '0' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 -BumpMin = '100 mwei' +EstimateLimit = false +BumpMin = '5 gwei' BumpPercent = 40 BumpThreshold = 3 EIP1559DynamicFees = false @@ -4554,11 +4931,14 @@ TipCapMin = '1 wei' [GasEstimator.BlockHistory] BatchSize = 25 -BlockHistorySize = 12 +BlockHistorySize = 8 CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '4s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -4576,6 +4956,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4610,6 +4991,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -4638,6 +5020,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4653,6 +5036,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4670,6 +5056,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4704,6 +5091,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -4732,6 +5120,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -4747,6 +5136,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -4764,6 +5156,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4780,35 +5173,37 @@ GasLimit = 5400000

-
Simulated (1337)

+

Bsquared Testnet (1123)

```toml AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -FinalityDepth = 10 -FinalityTagEnabled = false +ChainType = 'optimismBedrock' +FinalityDepth = 2000 +FinalityTagEnabled = true LogBackfillBatchSize = 1000 -LogPollInterval = '15s' +LogPollInterval = '5s' LogKeepBlocksDepth = 100000 LogPrunePageSize = 10000 BackupLogPollerBlockDelay = 100 -MinIncomingConfirmations = 1 -MinContractPayment = '100' +MinIncomingConfirmations = 3 +MinContractPayment = '0.00001 link' NonceAutoSync = true -NoNewHeadsThreshold = '0s' +NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 -NoNewFinalizedHeadsThreshold = '0s' +NoNewFinalizedHeadsThreshold = '1h10m0s' [Transactions] ForwardersEnabled = false MaxInFlight = 16 MaxQueued = 250 ReaperInterval = '1h0m0s' -ReaperThreshold = '0s' -ResendAfterThreshold = '0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' [Transactions.AutoPurge] Enabled = false @@ -4817,33 +5212,37 @@ Enabled = false Enabled = true [GasEstimator] -Mode = 'FixedPrice' +Mode = 'FeeHistory' PriceDefault = '20 gwei' -PriceMax = '100 micro' -PriceMin = '0' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 gwei' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 -BumpThreshold = 0 -EIP1559DynamicFees = false -FeeCapDefault = '100 micro' +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' TipCapDefault = '1 wei' TipCapMin = '1 wei' [GasEstimator.BlockHistory] BatchSize = 25 -BlockHistorySize = 8 +BlockHistorySize = 100 CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '2s' + [HeadTracker] -HistoryDepth = 10 -MaxBufferSize = 100 -SamplingInterval = '0s' +HistoryDepth = 100 +MaxBufferSize = 3 +SamplingInterval = '1s' MaxAllowedFinalityDepth = 10000 FinalityTagBypass = true @@ -4857,9 +5256,10 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] -ContractConfirmations = 1 +ContractConfirmations = 4 ContractTransmitterTransmitTimeout = '10s' DatabaseTimeout = '10s' DeltaCOverride = '168h0m0s' @@ -4873,28 +5273,29 @@ GasLimit = 5400000

-
Polygon Zkevm Goerli (1442)

+

Unichain Testnet (1301)

```toml AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -ChainType = 'zkevm' -FinalityDepth = 500 -FinalityTagEnabled = false +ChainType = 'optimismBedrock' +FinalityDepth = 2000 +FinalityTagEnabled = true LogBackfillBatchSize = 1000 -LogPollInterval = '30s' +LogPollInterval = '5s' LogKeepBlocksDepth = 100000 LogPrunePageSize = 10000 BackupLogPollerBlockDelay = 100 -MinIncomingConfirmations = 1 +MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true -NoNewHeadsThreshold = '12m0s' -RPCDefaultBatchSize = 100 +NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 -NoNewFinalizedHeadsThreshold = '0s' +NoNewFinalizedHeadsThreshold = '10m0s' [Transactions] ForwardersEnabled = false @@ -4902,7 +5303,7 @@ MaxInFlight = 16 MaxQueued = 250 ReaperInterval = '1h0m0s' ReaperThreshold = '168h0m0s' -ResendAfterThreshold = '3m0s' +ResendAfterThreshold = '1m0s' [Transactions.AutoPurge] Enabled = false @@ -4911,31 +5312,35 @@ Enabled = false Enabled = true [GasEstimator] -Mode = 'BlockHistory' +Mode = 'FeeHistory' PriceDefault = '20 gwei' PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' -PriceMin = '50 mwei' +PriceMin = '1 gwei' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 -BumpMin = '20 mwei' -BumpPercent = 40 +EstimateLimit = false +BumpMin = '5 gwei' +BumpPercent = 20 BumpThreshold = 3 -EIP1559DynamicFees = false +EIP1559DynamicFees = true FeeCapDefault = '100 gwei' TipCapDefault = '1 wei' TipCapMin = '1 wei' [GasEstimator.BlockHistory] BatchSize = 25 -BlockHistorySize = 12 +BlockHistorySize = 100 CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '1s' + [HeadTracker] -HistoryDepth = 2000 +HistoryDepth = 100 MaxBufferSize = 3 SamplingInterval = '1s' MaxAllowedFinalityDepth = 10000 @@ -4951,6 +5356,106 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5400000 +``` + +

+ +
Simulated (1337)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +FinalityDepth = 10 +FinalityTagEnabled = false +LogBackfillBatchSize = 1000 +LogPollInterval = '15s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 10000 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 1 +MinContractPayment = '100' +NonceAutoSync = true +NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '0s' +ResendAfterThreshold = '0s' + +[Transactions.AutoPurge] +Enabled = false + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'FixedPrice' +PriceDefault = '20 gwei' +PriceMax = '100 micro' +PriceMin = '0' +LimitDefault = 8000000 +LimitMax = 8000000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 0 +EIP1559DynamicFees = false +FeeCapDefault = '100 micro' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 8 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + +[HeadTracker] +HistoryDepth = 10 +MaxBufferSize = 100 +SamplingInterval = '0s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -4985,6 +5490,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -5013,6 +5519,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -5028,6 +5535,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 400 MaxBufferSize = 3 @@ -5045,6 +5555,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5079,6 +5590,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '12m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 100 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -5099,15 +5611,16 @@ Enabled = false Enabled = true [GasEstimator] -Mode = 'BlockHistory' +Mode = 'FeeHistory' PriceDefault = '20 gwei' PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' -PriceMin = '1 mwei' +PriceMin = '0' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 -BumpMin = '20 mwei' +EstimateLimit = false +BumpMin = '5 gwei' BumpPercent = 40 BumpThreshold = 3 EIP1559DynamicFees = false @@ -5117,11 +5630,14 @@ TipCapMin = '1 wei' [GasEstimator.BlockHistory] BatchSize = 25 -BlockHistorySize = 12 +BlockHistorySize = 8 CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '4s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -5139,6 +5655,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5173,6 +5690,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 @@ -5201,6 +5719,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -5216,6 +5735,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5233,6 +5755,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -5249,6 +5772,106 @@ GasLimit = 3800000

+
Worldchain Testnet (4801)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'optimismBedrock' +FinalityDepth = 2500 +FinalityTagEnabled = true +LogBackfillBatchSize = 1000 +LogPollInterval = '5s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 10000 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 3 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '1h30m0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' + +[Transactions.AutoPurge] +Enabled = false + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'FeeHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 gwei' +LimitDefault = 8000000 +LimitMax = 8000000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 100 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '2s' + +[HeadTracker] +HistoryDepth = 100 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5400000 +``` + +

+
Mantle Sepolia (5003)

```toml @@ -5267,6 +5890,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -5295,6 +5919,7 @@ LimitDefault = 100000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -5311,6 +5936,9 @@ CheckInclusionPercentile = 90 EIP1559FeeCapBufferBlocks = 0 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 600 MaxBufferSize = 3 @@ -5328,6 +5956,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -5361,6 +5990,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -5389,6 +6019,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5404,6 +6035,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5421,6 +6055,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5455,6 +6090,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -5483,6 +6119,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -5498,6 +6135,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -5515,6 +6155,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5549,6 +6190,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -5577,6 +6219,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -5592,6 +6235,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5609,6 +6255,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -5644,6 +6291,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -5672,6 +6320,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5687,6 +6336,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5704,6 +6356,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5739,6 +6392,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -5767,6 +6421,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5782,6 +6437,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5799,6 +6457,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -5833,6 +6492,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -5861,6 +6521,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 60 @@ -5877,6 +6538,9 @@ CheckInclusionPercentile = 90 EIP1559FeeCapBufferBlocks = 0 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -5894,6 +6558,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -5929,6 +6594,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -5957,6 +6623,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -5972,6 +6639,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -5989,6 +6659,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6023,6 +6694,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '1m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -6051,6 +6723,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '2 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6066,6 +6739,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -6083,6 +6759,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6117,6 +6794,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 @@ -6145,6 +6823,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6160,6 +6839,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6177,6 +6859,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6211,6 +6894,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 @@ -6239,6 +6923,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6254,6 +6939,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6271,6 +6959,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6305,6 +6994,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '1m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -6333,6 +7023,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '2 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6348,6 +7039,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -6365,6 +7059,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6381,27 +7076,30 @@ GasLimit = 5400000

-
Linea Goerli (59140)

+

Zircuit Sepolia (48899)

```toml AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -FinalityDepth = 15 -FinalityTagEnabled = false +ChainType = 'optimismBedrock' +FinalityDepth = 1000 +FinalityTagEnabled = true +LinkContractAddress = '0xDEE94506570cA186BC1e3516fCf4fd719C312cCD' LogBackfillBatchSize = 1000 -LogPollInterval = '15s' +LogPollInterval = '2s' LogKeepBlocksDepth = 100000 LogPrunePageSize = 10000 BackupLogPollerBlockDelay = 100 MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true -NoNewHeadsThreshold = '0s' +NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 -NoNewFinalizedHeadsThreshold = '0s' +NoNewFinalizedHeadsThreshold = '15m0s' [Transactions] ForwardersEnabled = false @@ -6409,10 +7107,12 @@ MaxInFlight = 16 MaxQueued = 250 ReaperInterval = '1h0m0s' ReaperThreshold = '168h0m0s' -ResendAfterThreshold = '3m0s' +ResendAfterThreshold = '30s' [Transactions.AutoPurge] -Enabled = false +Enabled = true +Threshold = 90 +MinAttempts = 3 [BalanceMonitor] Enabled = true @@ -6421,11 +7121,214 @@ Enabled = true Mode = 'BlockHistory' PriceDefault = '20 gwei' PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' -PriceMin = '1 gwei' +PriceMin = '1 wei' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '100 wei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 60 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + +[HeadTracker] +HistoryDepth = 2000 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 10 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 1 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 6500000 +``` + +

+ +
Zircuit Mainnet (48900)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'optimismBedrock' +FinalityDepth = 1000 +FinalityTagEnabled = true +LinkContractAddress = '0x5D6d033B4FbD2190D99D930719fAbAcB64d2439a' +LogBackfillBatchSize = 1000 +LogPollInterval = '2s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 10000 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 3 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '15m0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '30s' + +[Transactions.AutoPurge] +Enabled = true +Threshold = 90 +MinAttempts = 3 + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 wei' +LimitDefault = 8000000 +LimitMax = 8000000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '100 wei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 24 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + +[HeadTracker] +HistoryDepth = 2000 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 10 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 1 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 6500000 +``` + +

+ +
Linea Goerli (59140)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +FinalityDepth = 15 +FinalityTagEnabled = false +LogBackfillBatchSize = 1000 +LogPollInterval = '15s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 10000 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 3 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '3m0s' + +[Transactions.AutoPurge] +Enabled = false + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 gwei' +LimitDefault = 8000000 +LimitMax = 8000000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 40 BumpThreshold = 3 @@ -6441,6 +7344,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6458,6 +7364,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -6491,6 +7398,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -6514,28 +7422,231 @@ Enabled = true Mode = 'BlockHistory' PriceDefault = '20 gwei' PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' -PriceMin = '1 wei' +PriceMin = '1 wei' +LimitDefault = 8000000 +LimitMax = 8000000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 8 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + +[HeadTracker] +HistoryDepth = 1000 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5400000 +``` + +

+ +
Linea Mainnet (59144)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +FinalityDepth = 300 +FinalityTagEnabled = false +LogBackfillBatchSize = 1000 +LogPollInterval = '15s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 10000 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 3 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '3m0s' + +[Transactions.AutoPurge] +Enabled = false + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '400 mwei' +LimitDefault = 8000000 +LimitMax = 8000000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '5 gwei' +BumpPercent = 40 +BumpThreshold = 3 +EIP1559DynamicFees = false +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 8 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + +[HeadTracker] +HistoryDepth = 350 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5400000 +``` + +

+ +
Metis Sepolia (59902)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'optimismBedrock' +FinalityDepth = 10 +FinalityTagEnabled = true +LogBackfillBatchSize = 1000 +LogPollInterval = '15s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 10000 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 1 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' + +[Transactions.AutoPurge] +Enabled = false + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'SuggestedPrice' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '0' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 -EIP1559DynamicFees = true +EIP1559DynamicFees = false FeeCapDefault = '100 gwei' TipCapDefault = '1 wei' TipCapMin = '1 wei' [GasEstimator.BlockHistory] BatchSize = 25 -BlockHistorySize = 8 +BlockHistorySize = 0 CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] -HistoryDepth = 1000 +HistoryDepth = 100 MaxBufferSize = 3 SamplingInterval = '1s' MaxAllowedFinalityDepth = 10000 @@ -6545,15 +7656,16 @@ FinalityTagBypass = true PollFailureThreshold = 5 PollInterval = '10s' SelectionMode = 'HighestHead' -SyncThreshold = 5 +SyncThreshold = 10 LeaseDuration = '0s' NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] -ContractConfirmations = 4 +ContractConfirmations = 1 ContractTransmitterTransmitTimeout = '10s' DatabaseTimeout = '10s' DeltaCOverride = '168h0m0s' @@ -6567,27 +7679,29 @@ GasLimit = 5400000

-
Linea Mainnet (59144)

+

BOB Mainnet (60808)

```toml AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -FinalityDepth = 300 -FinalityTagEnabled = false +ChainType = 'optimismBedrock' +FinalityDepth = 900 +FinalityTagEnabled = true LogBackfillBatchSize = 1000 -LogPollInterval = '15s' +LogPollInterval = '5s' LogKeepBlocksDepth = 100000 LogPrunePageSize = 10000 BackupLogPollerBlockDelay = 100 MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true -NoNewHeadsThreshold = '0s' +NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 -NoNewFinalizedHeadsThreshold = '0s' +NoNewFinalizedHeadsThreshold = '35m0s' [Transactions] ForwardersEnabled = false @@ -6595,7 +7709,7 @@ MaxInFlight = 16 MaxQueued = 250 ReaperInterval = '1h0m0s' ReaperThreshold = '168h0m0s' -ResendAfterThreshold = '3m0s' +ResendAfterThreshold = '1m0s' [Transactions.AutoPurge] Enabled = false @@ -6604,31 +7718,35 @@ Enabled = false Enabled = true [GasEstimator] -Mode = 'BlockHistory' +Mode = 'FeeHistory' PriceDefault = '20 gwei' PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' -PriceMin = '400 mwei' +PriceMin = '1 gwei' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' -BumpPercent = 40 +BumpPercent = 20 BumpThreshold = 3 -EIP1559DynamicFees = false +EIP1559DynamicFees = true FeeCapDefault = '100 gwei' TipCapDefault = '1 wei' TipCapMin = '1 wei' [GasEstimator.BlockHistory] BatchSize = 25 -BlockHistorySize = 8 +BlockHistorySize = 100 CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '2s' + [HeadTracker] -HistoryDepth = 350 +HistoryDepth = 100 MaxBufferSize = 3 SamplingInterval = '1s' MaxAllowedFinalityDepth = 10000 @@ -6644,6 +7762,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -6660,17 +7779,17 @@ GasLimit = 5400000

-
Metis Sepolia (59902)

+

Avalanche ANZ testnet (76578)

```toml AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -ChainType = 'metis' -FinalityDepth = 10 -FinalityTagEnabled = true +FinalityDepth = 1 +FinalityTagEnabled = false +LinkContractAddress = '0x779877A7B0D9E8603169DdbD7836e478b4624789' LogBackfillBatchSize = 1000 -LogPollInterval = '15s' +LogPollInterval = '3s' LogKeepBlocksDepth = 100000 LogPrunePageSize = 10000 BackupLogPollerBlockDelay = 100 @@ -6678,8 +7797,9 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 -RPCBlockQueryDelay = 1 +RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' @@ -6698,14 +7818,15 @@ Enabled = false Enabled = true [GasEstimator] -Mode = 'SuggestedPrice' -PriceDefault = '20 gwei' +Mode = 'BlockHistory' +PriceDefault = '25 gwei' PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' -PriceMin = '0' +PriceMin = '25 gwei' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -6716,11 +7837,14 @@ TipCapMin = '1 wei' [GasEstimator.BlockHistory] BatchSize = 25 -BlockHistorySize = 0 +BlockHistorySize = 24 CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -6732,12 +7856,13 @@ FinalityTagBypass = true PollFailureThreshold = 5 PollInterval = '10s' SelectionMode = 'HighestHead' -SyncThreshold = 10 +SyncThreshold = 5 LeaseDuration = '0s' NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -6754,33 +7879,34 @@ GasLimit = 5400000

-
Avalanche ANZ testnet (76578)

+

Polygon Mumbai (80001)

```toml AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -FinalityDepth = 1 -FinalityTagEnabled = false -LinkContractAddress = '0x779877A7B0D9E8603169DdbD7836e478b4624789' +FinalityDepth = 500 +FinalityTagEnabled = true +LinkContractAddress = '0x326C977E6efc84E512bB9C30f76E30c160eD06FB' LogBackfillBatchSize = 1000 -LogPollInterval = '3s' +LogPollInterval = '1s' LogKeepBlocksDepth = 100000 LogPrunePageSize = 10000 BackupLogPollerBlockDelay = 100 -MinIncomingConfirmations = 1 +MinIncomingConfirmations = 5 MinContractPayment = '0.00001 link' NonceAutoSync = true -NoNewHeadsThreshold = '0s' -RPCDefaultBatchSize = 250 -RPCBlockQueryDelay = 2 +NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 100 +RPCBlockQueryDelay = 10 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' [Transactions] ForwardersEnabled = false MaxInFlight = 16 -MaxQueued = 250 +MaxQueued = 5000 ReaperInterval = '1h0m0s' ReaperThreshold = '168h0m0s' ResendAfterThreshold = '1m0s' @@ -6800,9 +7926,10 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 -BumpMin = '5 gwei' +EstimateLimit = false +BumpMin = '20 gwei' BumpPercent = 20 -BumpThreshold = 3 +BumpThreshold = 5 EIP1559DynamicFees = false FeeCapDefault = '100 gwei' TipCapDefault = '1 wei' @@ -6815,8 +7942,11 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] -HistoryDepth = 100 +HistoryDepth = 2000 MaxBufferSize = 3 SamplingInterval = '1s' MaxAllowedFinalityDepth = 10000 @@ -6826,15 +7956,16 @@ FinalityTagBypass = true PollFailureThreshold = 5 PollInterval = '10s' SelectionMode = 'HighestHead' -SyncThreshold = 5 +SyncThreshold = 10 LeaseDuration = '0s' NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] -ContractConfirmations = 1 +ContractConfirmations = 4 ContractTransmitterTransmitTimeout = '10s' DatabaseTimeout = '10s' DeltaCOverride = '168h0m0s' @@ -6848,15 +7979,14 @@ GasLimit = 5400000

-
Polygon Mumbai (80001)

+

Polygon Amoy (80002)

```toml AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false FinalityDepth = 500 -FinalityTagEnabled = true -LinkContractAddress = '0x326C977E6efc84E512bB9C30f76E30c160eD06FB' +FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '1s' LogKeepBlocksDepth = 100000 @@ -6866,10 +7996,11 @@ MinIncomingConfirmations = 5 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 100 RPCBlockQueryDelay = 10 FinalizedBlockOffset = 0 -NoNewFinalizedHeadsThreshold = '0s' +NoNewFinalizedHeadsThreshold = '12m0s' [Transactions] ForwardersEnabled = false @@ -6894,10 +8025,11 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '20 gwei' BumpPercent = 20 BumpThreshold = 5 -EIP1559DynamicFees = false +EIP1559DynamicFees = true FeeCapDefault = '100 gwei' TipCapDefault = '1 wei' TipCapMin = '1 wei' @@ -6909,6 +8041,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 2000 MaxBufferSize = 3 @@ -6926,6 +8061,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -6942,32 +8078,33 @@ GasLimit = 5400000

-
Polygon Amoy (80002)

+

Berachain Testnet (80084)

```toml AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -FinalityDepth = 500 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 -LogPollInterval = '1s' +LogPollInterval = '10s' LogKeepBlocksDepth = 100000 LogPrunePageSize = 10000 BackupLogPollerBlockDelay = 100 -MinIncomingConfirmations = 5 +MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true -NoNewHeadsThreshold = '30s' -RPCDefaultBatchSize = 100 -RPCBlockQueryDelay = 10 +NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 -NoNewFinalizedHeadsThreshold = '12m0s' +NoNewFinalizedHeadsThreshold = '5m0s' [Transactions] ForwardersEnabled = false MaxInFlight = 16 -MaxQueued = 5000 +MaxQueued = 250 ReaperInterval = '1h0m0s' ReaperThreshold = '168h0m0s' ResendAfterThreshold = '1m0s' @@ -6979,17 +8116,18 @@ Enabled = false Enabled = true [GasEstimator] -Mode = 'BlockHistory' -PriceDefault = '25 gwei' +Mode = 'FeeHistory' +PriceDefault = '20 gwei' PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' -PriceMin = '25 gwei' +PriceMin = '1 gwei' LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 -BumpMin = '20 gwei' +EstimateLimit = false +BumpMin = '5 gwei' BumpPercent = 20 -BumpThreshold = 5 +BumpThreshold = 3 EIP1559DynamicFees = true FeeCapDefault = '100 gwei' TipCapDefault = '1 wei' @@ -6997,13 +8135,16 @@ TipCapMin = '1 wei' [GasEstimator.BlockHistory] BatchSize = 25 -BlockHistorySize = 24 +BlockHistorySize = 100 CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '5s' + [HeadTracker] -HistoryDepth = 2000 +HistoryDepth = 100 MaxBufferSize = 3 SamplingInterval = '1s' MaxAllowedFinalityDepth = 10000 @@ -7013,12 +8154,13 @@ FinalityTagBypass = true PollFailureThreshold = 5 PollInterval = '10s' SelectionMode = 'HighestHead' -SyncThreshold = 10 +SyncThreshold = 5 LeaseDuration = '0s' NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -7053,6 +8195,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -7081,6 +8224,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 60 @@ -7097,6 +8241,9 @@ CheckInclusionPercentile = 90 EIP1559FeeCapBufferBlocks = 0 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -7114,6 +8261,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -7148,6 +8296,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -7176,6 +8325,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -7191,6 +8341,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -7208,6 +8361,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7243,6 +8397,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -7271,6 +8426,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -7286,6 +8442,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -7303,6 +8462,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7338,6 +8498,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -7366,6 +8527,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7381,6 +8543,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7398,6 +8563,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7433,6 +8599,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -7461,6 +8628,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7476,6 +8644,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7493,6 +8664,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7528,6 +8700,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -7556,6 +8729,7 @@ LimitDefault = 8000000 LimitMax = 1000000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 5 @@ -7571,6 +8745,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7588,6 +8765,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7622,6 +8800,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -7650,6 +8829,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '1 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -7665,6 +8845,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -7682,6 +8865,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7716,6 +8900,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -7744,6 +8929,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '1 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -7759,6 +8945,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 50 MaxBufferSize = 3 @@ -7776,6 +8965,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7792,6 +8982,106 @@ GasLimit = 5400000

+
BOB Testnet (808813)

+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'optimismBedrock' +FinalityDepth = 900 +FinalityTagEnabled = true +LogBackfillBatchSize = 1000 +LogPollInterval = '5s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 10000 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 3 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '35m0s' + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' + +[Transactions.AutoPurge] +Enabled = false + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'FeeHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 gwei' +LimitDefault = 8000000 +LimitMax = 8000000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateLimit = false +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 100 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '2s' + +[HeadTracker] +HistoryDepth = 100 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5400000 +``` + +

+
Ethereum Sepolia (11155111)

```toml @@ -7810,6 +9100,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -7838,6 +9129,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -7853,6 +9145,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -7870,6 +9165,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -7904,6 +9200,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '40s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -7932,6 +9229,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 3 @@ -7947,6 +9245,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -7964,6 +9265,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 1 @@ -7998,6 +9300,7 @@ MinIncomingConfirmations = 3 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -8026,6 +9329,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '100 wei' BumpPercent = 20 BumpThreshold = 60 @@ -8042,6 +9346,9 @@ CheckInclusionPercentile = 90 EIP1559FeeCapBufferBlocks = 0 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 300 MaxBufferSize = 3 @@ -8059,6 +9366,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -8093,6 +9401,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -8121,6 +9430,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -8136,6 +9446,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -8153,6 +9466,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -8187,6 +9501,7 @@ MinIncomingConfirmations = 1 MinContractPayment = '0.00001 link' NonceAutoSync = true NoNewHeadsThreshold = '30s' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -8215,6 +9530,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -8230,6 +9546,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 60 +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + [HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -8247,6 +9566,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [OCR] ContractConfirmations = 4 @@ -8452,6 +9772,12 @@ The latest finalized block on chain is 64, so block 63 is the latest finalized f Block 64 will be treated as finalized by CL Node only when chain's latest finalized block is 65. As chain finalizes blocks in batches of 32, CL Node has to wait for a whole new batch to be finalized to treat block 64 as finalized. +### LogBroadcasterEnabled +```toml +LogBroadcasterEnabled = true # Default +``` +LogBroadcasterEnabled is a feature flag for LogBroadcaster, by default it's true. + ### NoNewFinalizedHeadsThreshold ```toml NoNewFinalizedHeadsThreshold = '0' # Default @@ -8577,6 +9903,7 @@ LimitDefault = 8_000_000 # Default LimitMax = 8_000_000 # Default LimitMultiplier = '1.0' # Default LimitTransfer = 21_000 # Default +EstimateLimit = false # Default BumpMin = '5 gwei' # Default BumpPercent = 20 # Default BumpThreshold = 3 # Default @@ -8671,6 +9998,12 @@ LimitTransfer = 21_000 # Default ``` LimitTransfer is the gas limit used for an ordinary ETH transfer. +### EstimateLimit +```toml +EstimateLimit = false # Default +``` +EstimateLimit enables estimating gas limits for transactions. This feature respects the gas limit provided during transaction creation as an upper bound. + ### BumpMin ```toml BumpMin = '5 gwei' # Default @@ -8884,6 +10217,24 @@ Setting this number higher will cause the Chainlink node to select higher gas pr Setting it lower will tend to set lower gas prices. +## EVM.GasEstimator.FeeHistory +```toml +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' # Default +``` + + +### CacheTimeout +```toml +CacheTimeout = '10s' # Default +``` +CacheTimeout is the time to wait in order to refresh the cached values stored in the FeeHistory estimator. A small jitter is applied so the timeout won't be exactly the same each time. + +You want this value to be close to the block time. For slower chains, like Ethereum, you can set it to 12s, the same as the block time. For faster chains you can skip a block or two +and set it to two times the block time i.e. on Optimism you can set it to 4s. Ideally, you don't want to go lower than 1s since the RTT times of the RPC requests will be comparable to +the timeout. The estimator is already adding a buffer to account for a potential increase in prices within one or two blocks. On the other hand, slower frequency will fail to refresh +the prices and end up in stale values. + ## EVM.HeadTracker ```toml [EVM.HeadTracker] @@ -8970,6 +10321,7 @@ NodeIsSyncingEnabled = false # Default FinalizedBlockPollInterval = '5s' # Default EnforceRepeatableRead = false # Default DeathDeclarationDelay = '10s' # Default +NewHeadsPollInterval = '0s' # Default ``` The node pool manages multiple RPC endpoints. @@ -9062,6 +10414,14 @@ Larger values might be helpful to reduce the noisiness of health checks like `En trigger declaration of `FinalizedBlockOutOfSync` due to insignificant network delays in broadcasting of the finalized state among RPCs. RPC will not be picked to handle a request even if this option is set to a nonzero value. +### NewHeadsPollInterval +```toml +NewHeadsPollInterval = '0s' # Default +``` +NewHeadsPollInterval define an interval for polling new block periodically using http client rather than subscribe to ws feed + +Set to 0 to disable. + ## EVM.NodePool.Errors :warning: **_ADVANCED_**: _Do not change these settings unless you know what you are doing._ ```toml @@ -9240,7 +10600,7 @@ Name is a unique (per-chain) identifier for this node. ```toml WSURL = 'wss://web.socket/test' # Example ``` -WSURL is the WS(S) endpoint for this node. Required for primary nodes. +WSURL is the WS(S) endpoint for this node. Required for primary nodes when `LogBroadcasterEnabled` is `true` ### HTTPURL ```toml diff --git a/go.mod b/go.mod index 683793cb53..af1174beab 100644 --- a/go.mod +++ b/go.mod @@ -72,7 +72,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/shirou/gopsutil/v3 v3.24.3 github.com/shopspring/decimal v1.4.0 - github.com/smartcontractkit/chain-selectors v1.0.21 + github.com/smartcontractkit/chain-selectors v1.0.27 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f github.com/smartcontractkit/chainlink-common v0.2.1-0.20240717132349-ee5af9b79834 diff --git a/go.sum b/go.sum index 5ad6772ecd..b81f4930a0 100644 --- a/go.sum +++ b/go.sum @@ -1023,8 +1023,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartcontractkit/chain-selectors v1.0.21 h1:KCR9SA7PhOexaBzFieHoLv1WonwhVOPtOStpqTmLC4E= -github.com/smartcontractkit/chain-selectors v1.0.21/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= +github.com/smartcontractkit/chain-selectors v1.0.27 h1:VE/ftX9Aae4gnw67yR1raKi+30iWKL/sWq8uyiLHM8k= +github.com/smartcontractkit/chain-selectors v1.0.27/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f h1:lQZBOjeYFpCdk0mGQUhbrJipd00tu49xK4zSijC/9Co= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 7abf19dc82..c26b3d27e9 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -34,7 +34,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/slack-go/slack v0.12.2 github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240808195812-ae0378684685 - github.com/smartcontractkit/chain-selectors v1.0.21 + github.com/smartcontractkit/chain-selectors v1.0.27 github.com/smartcontractkit/chainlink-automation v1.0.4 github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f github.com/smartcontractkit/chainlink-common v0.2.1-0.20240717132349-ee5af9b79834 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index babc3fc8c6..381abac5be 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1390,8 +1390,8 @@ github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240808195812-ae0378684685 h1:jakAsdhDxV4cMgRAcSvHraXjyePi8umG5SEUTGFvuy8= github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240808195812-ae0378684685/go.mod h1:p7L/xNEQpHDdZtgFA6/FavuZHqvV3kYhQysxBywmq1k= -github.com/smartcontractkit/chain-selectors v1.0.21 h1:KCR9SA7PhOexaBzFieHoLv1WonwhVOPtOStpqTmLC4E= -github.com/smartcontractkit/chain-selectors v1.0.21/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= +github.com/smartcontractkit/chain-selectors v1.0.27 h1:VE/ftX9Aae4gnw67yR1raKi+30iWKL/sWq8uyiLHM8k= +github.com/smartcontractkit/chain-selectors v1.0.27/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f h1:lQZBOjeYFpCdk0mGQUhbrJipd00tu49xK4zSijC/9Co= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index a90826f599..d086677434 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -374,7 +374,7 @@ require ( github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/smartcontractkit/chain-selectors v1.0.21 // indirect + github.com/smartcontractkit/chain-selectors v1.0.27 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240718160222-2dc0c8136bfa // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240710170203-5b41615da827 // indirect github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240712132946-267a37c5ac6e // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 536b96f8b6..b181bfbaae 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1354,8 +1354,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ= github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= -github.com/smartcontractkit/chain-selectors v1.0.21 h1:KCR9SA7PhOexaBzFieHoLv1WonwhVOPtOStpqTmLC4E= -github.com/smartcontractkit/chain-selectors v1.0.21/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= +github.com/smartcontractkit/chain-selectors v1.0.27 h1:VE/ftX9Aae4gnw67yR1raKi+30iWKL/sWq8uyiLHM8k= +github.com/smartcontractkit/chain-selectors v1.0.27/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.4 h1:iyW181JjKHLNMnDleI8umfIfVVlwC7+n5izbLSFgjw8= github.com/smartcontractkit/chainlink-automation v1.0.4/go.mod h1:u4NbPZKJ5XiayfKHD/v3z3iflQWqvtdhj13jVZXj/cM= github.com/smartcontractkit/chainlink-ccip v0.0.0-20240816163757-48726fd8165f h1:lQZBOjeYFpCdk0mGQUhbrJipd00tu49xK4zSijC/9Co= diff --git a/integration-tests/testconfig/vrfv2plus/vrfv2plus.toml b/integration-tests/testconfig/vrfv2plus/vrfv2plus.toml index 8f8aa9530e..cd089013db 100644 --- a/integration-tests/testconfig/vrfv2plus/vrfv2plus.toml +++ b/integration-tests/testconfig/vrfv2plus/vrfv2plus.toml @@ -19,6 +19,7 @@ MaxSize = '0b' [WebServer] AllowOrigins = '*' HTTPPort = 6688 +HTTPWriteTimeout = '1m0s' SecureCookies = false [WebServer.RateLimit] diff --git a/package.json b/package.json index 541cfb1aaf..e86ba1ac33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccip", - "version": "2.14.0-ccip1.5.0", + "version": "2.14.0-ccip1.5.6", "description": "node of the decentralized oracle network, bridging on and off-chain computation", "main": "index.js", "scripts": { diff --git a/testdata/scripts/health/default.txtar b/testdata/scripts/health/default.txtar index 1dbf6b8eb9..777d3e5e12 100644 --- a/testdata/scripts/health/default.txtar +++ b/testdata/scripts/health/default.txtar @@ -31,6 +31,7 @@ fj293fbBnlQ!f9vNs HTTPPort = $PORT -- out.txt -- +ok HeadReporter ok JobSpawner ok Mailbox.Monitor ok Mercury.WSRPCPool @@ -38,12 +39,20 @@ ok Mercury.WSRPCPool.CacheSet ok PipelineORM ok PipelineRunner ok PipelineRunner.BridgeCache -ok PromReporter ok TelemetryManager -- out.json -- { "data": [ + { + "type": "checks", + "id": "HeadReporter", + "attributes": { + "name": "HeadReporter", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "JobSpawner", @@ -107,15 +116,6 @@ ok TelemetryManager "output": "" } }, - { - "type": "checks", - "id": "PromReporter", - "attributes": { - "name": "PromReporter", - "status": "passing", - "output": "" - } - }, { "type": "checks", "id": "TelemetryManager", diff --git a/testdata/scripts/health/multi-chain.txtar b/testdata/scripts/health/multi-chain.txtar index 8178f8e821..3bd15850af 100644 --- a/testdata/scripts/health/multi-chain.txtar +++ b/testdata/scripts/health/multi-chain.txtar @@ -75,6 +75,7 @@ ok EVM.1.Txm.BlockHistoryEstimator ok EVM.1.Txm.Broadcaster ok EVM.1.Txm.Confirmer ok EVM.1.Txm.WrappedEvmEstimator +ok HeadReporter ok JobSpawner ok Mailbox.Monitor ok Mercury.WSRPCPool @@ -82,7 +83,6 @@ ok Mercury.WSRPCPool.CacheSet ok PipelineORM ok PipelineRunner ok PipelineRunner.BridgeCache -ok PromReporter ok Solana.Bar ok StarkNet.Baz ok TelemetryManager @@ -216,6 +216,15 @@ ok TelemetryManager "output": "" } }, + { + "type": "checks", + "id": "HeadReporter", + "attributes": { + "name": "HeadReporter", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "JobSpawner", @@ -279,15 +288,6 @@ ok TelemetryManager "output": "" } }, - { - "type": "checks", - "id": "PromReporter", - "attributes": { - "name": "PromReporter", - "status": "passing", - "output": "" - } - }, { "type": "checks", "id": "Solana.Bar", diff --git a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar index fe522ca1bb..fb02650ee7 100644 --- a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar @@ -331,6 +331,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x3E64Cd889482443324F91bFA9c84fE72A511f48A' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -359,6 +360,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -374,6 +376,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -391,6 +396,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar index 5a5ce42d6b..8f4f2010de 100644 --- a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar @@ -331,6 +331,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x3E64Cd889482443324F91bFA9c84fE72A511f48A' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -359,6 +360,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -374,6 +376,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -391,6 +396,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/testdata/scripts/node/validate/disk-based-logging.txtar b/testdata/scripts/node/validate/disk-based-logging.txtar index 12773dc99c..8f2ac33f33 100644 --- a/testdata/scripts/node/validate/disk-based-logging.txtar +++ b/testdata/scripts/node/validate/disk-based-logging.txtar @@ -331,6 +331,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x3E64Cd889482443324F91bFA9c84fE72A511f48A' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -359,6 +360,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -374,6 +376,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -391,6 +396,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/testdata/scripts/node/validate/invalid.txtar b/testdata/scripts/node/validate/invalid.txtar index e709b24d5b..563f075bc0 100644 --- a/testdata/scripts/node/validate/invalid.txtar +++ b/testdata/scripts/node/validate/invalid.txtar @@ -321,6 +321,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x3E64Cd889482443324F91bFA9c84fE72A511f48A' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -349,6 +350,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -364,6 +366,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -381,6 +386,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4 diff --git a/testdata/scripts/node/validate/valid.txtar b/testdata/scripts/node/validate/valid.txtar index 9b22da4850..c27cbd51e1 100644 --- a/testdata/scripts/node/validate/valid.txtar +++ b/testdata/scripts/node/validate/valid.txtar @@ -328,6 +328,7 @@ MinContractPayment = '0.1 link' NonceAutoSync = true NoNewHeadsThreshold = '3m0s' OperatorFactoryAddress = '0x3E64Cd889482443324F91bFA9c84fE72A511f48A' +LogBroadcasterEnabled = true RPCDefaultBatchSize = 250 RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 @@ -356,6 +357,7 @@ LimitDefault = 8000000 LimitMax = 8000000 LimitMultiplier = '1' LimitTransfer = 21000 +EstimateLimit = false BumpMin = '5 gwei' BumpPercent = 20 BumpThreshold = 3 @@ -371,6 +373,9 @@ CheckInclusionBlocks = 12 CheckInclusionPercentile = 90 TransactionPercentile = 50 +[EVM.GasEstimator.FeeHistory] +CacheTimeout = '10s' + [EVM.HeadTracker] HistoryDepth = 100 MaxBufferSize = 3 @@ -388,6 +393,7 @@ NodeIsSyncingEnabled = false FinalizedBlockPollInterval = '5s' EnforceRepeatableRead = false DeathDeclarationDelay = '10s' +NewHeadsPollInterval = '0s' [EVM.OCR] ContractConfirmations = 4