diff --git a/.github/workflows/ccip-live-network-tests.yml b/.github/workflows/ccip-live-network-tests.yml index 7d908d9718..089fcf739b 100644 --- a/.github/workflows/ccip-live-network-tests.yml +++ b/.github/workflows/ccip-live-network-tests.yml @@ -15,8 +15,8 @@ on: required: false type: choice options: - - 'load' - 'smoke' + - 'load' test_secrets_override_key: description: 'Key to run tests with custom test secrets' required: false @@ -30,8 +30,6 @@ concurrency: env: CHAINLINK_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink CHAINLINK_VERSION: ${{ github.sha }} - CHAINLINK_TEST_VERSION: ${{ github.sha }} - ENV_JOB_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink-ccip-tests:${{ github.sha }} INTERNAL_DOCKER_REPO: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com AWS_ECR_REPO_PUBLIC_REGISTRY: public.ecr.aws E2E_TEST_CHAINLINK_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink @@ -41,19 +39,21 @@ env: E2E_TEST_GRAFANA_BASE_URL: ${{ vars.GRAFANA_URL }} # Default private key test secret loaded from Github Secret as only security team has access to it. # this key secrets.QA_SHARED_803C_KEY has a story behind it. To know more, see CCIP-2875 and SECHD-16575 tickets. - E2E_TEST_ETHEREUM_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} E2E_TEST_ARBITRUM_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} - E2E_TEST_BASE_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} - E2E_TEST_WEMIX_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} E2E_TEST_AVALANCHE_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} - E2E_TEST_ZKSYNC_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} - E2E_TEST_MODE_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} + E2E_TEST_BASE_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} + E2E_TEST_BLAST_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} + E2E_TEST_CELO_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} + E2E_TEST_ETHEREUM_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} + E2E_TEST_GNOSIS_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} + E2E_TEST_KROMA_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} E2E_TEST_METIS_ANDROMEDA_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} + E2E_TEST_MODE_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} E2E_TEST_OPTIMISM_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} - E2E_TEST_KROMA_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} - E2E_TEST_GNOSIS_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} E2E_TEST_POLYGON_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} - E2E_TEST_BSC_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} + E2E_TEST_WEMIX_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} + E2E_TEST_ZKSYNC_MAINNET_WALLET_KEY: ${{ secrets.QA_SHARED_803C_KEY }} + jobs: build-chainlink: @@ -99,6 +99,7 @@ jobs: build-test-image: environment: integration + if: ${{ github.event_name == 'workflow_dispatch' && inputs.test_type == 'load' }} permissions: id-token: write contents: read @@ -133,8 +134,8 @@ jobs: matrix: config: [mainnet.toml] needs: [ build-chainlink, build-test-image ] - # if the event is a scheduled event or the test type is load and no previous job failed - if: ${{ (github.event_name == 'schedule' || inputs.test_type == 'load') && !contains(needs.*.result, 'failure') }} + # if the event is a workflow_dispatch event and the test type is load and no previous job failed + if: ${{ github.event_name == 'workflow_dispatch' && inputs.test_type == 'load' && !contains(needs.*.result, 'failure') }} permissions: issues: read checks: write @@ -147,6 +148,8 @@ jobs: SLACK_CHANNEL: ${{ secrets.QA_SLACK_CHANNEL }} TEST_LOG_LEVEL: info REF_NAME: ${{ github.head_ref || github.ref_name }} + CHAINLINK_TEST_VERSION: ${{ github.sha }} + ENV_JOB_IMAGE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink-ccip-tests:${{ github.sha }} ENV_JOB_IMAGE_BASE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink-ccip-tests steps: @@ -207,11 +210,11 @@ jobs: RR_CPU: 4 DETACH_RUNNER: true TEST_TRIGGERED_BY: ccip-load-test-ci - BASE64_CCIP_CONFIG_OVERRIDE: ${{ steps.set_override_config.outputs.base_64_override }},${{ steps.setup_create_base64_config_ccip.outputs.base64_config }} - TEST_BASE64_CCIP_CONFIG_OVERRIDE: ${{ steps.set_override_config.outputs.base_64_override }},${{ steps.setup_create_base64_config_ccip.outputs.base64_config }} + BASE64_CCIP_CONFIG_OVERRIDE: ${{ steps.setup_create_base64_config_ccip.outputs.base64_config }},${{ steps.set_override_config.outputs.base_64_override }} + TEST_BASE64_CCIP_CONFIG_OVERRIDE: ${{ steps.setup_create_base64_config_ccip.outputs.base64_config }},${{ steps.set_override_config.outputs.base_64_override }} E2E_TEST_GRAFANA_DASHBOARD_URL: "/d/6vjVx-1V8/ccip-long-running-tests" with: - test_command_to_run: cd ./integration-tests/ccip-tests && go test -v -timeout 70m -count=1 -json -run ^TestLoadCCIPStableRPS$ ./load 2>&1 | tee /tmp/gotest.log | gotestfmt + test_command_to_run: cd ./integration-tests/ccip-tests && go test -v -timeout 70m -count=1 -json -run ^TestLoadCCIPStableRPS$ ./load 2>&1 | tee /tmp/gotest.log | gotestloghelper -ci -singlepackage -hidepassingtests=false test_download_vendor_packages_command: cd ./integration-tests && go mod download # Other default test secrets loaded from dotenv Github Secret. test_secrets_defaults_base64: ${{ secrets.CCIP_DEFAULT_TEST_SECRETS }} @@ -229,12 +232,12 @@ jobs: should_cleanup: false ccip-smoke-test: - name: CCIP smoke Test + name: CCIP smoke Test ${{ matrix.lanes.name }} environment: integration runs-on: ubuntu-latest - needs: [ build-chainlink, build-test-image ] - # if the event is a scheduled event or the test type is load and no previous job failed - if: ${{ github.event_name == 'workflow_dispatch' && inputs.test_type == 'smoke' && !contains(needs.*.result, 'failure') }} + needs: [ build-chainlink ] + # if the event is a scheduled event or the test type is smoke and no previous job failed + if: ${{ (github.event_name == 'schedule' || inputs.test_type == 'smoke') && !contains(needs.*.result, 'failure') }} permissions: issues: read checks: write @@ -247,10 +250,69 @@ jobs: SLACK_CHANNEL: ${{ secrets.QA_SLACK_CHANNEL }} TEST_LOG_LEVEL: info REF_NAME: ${{ github.head_ref || github.ref_name }} - ENV_JOB_IMAGE_BASE: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/chainlink-ccip-tests - + strategy: + fail-fast: false + matrix: + lanes: + - name: 'ARBITRUM_MAINNET' + pairs: 'ARBITRUM_MAINNET,BSC_MAINNET;ARBITRUM_MAINNET,OPTIMISM_MAINNET' + enabled: true + phaseTimeout: 20m + - name: 'AVALANCHE_MAINNET' + pairs: 'AVALANCHE_MAINNET,ARBITRUM_MAINNET;AVALANCHE_MAINNET,BASE_MAINNET;AVALANCHE_MAINNET,BSC_MAINNET;AVALANCHE_MAINNET,OPTIMISM_MAINNET;AVALANCHE_MAINNET,POLYGON_MAINNET;AVALANCHE_MAINNET,WEMIX_MAINNET' + enabled: true + phaseTimeout: 20m + - name: 'BASE_MAINNET' + pairs: 'BASE_MAINNET,ARBITRUM_MAINNET;BASE_MAINNET,BSC_MAINNET;BASE_MAINNET,OPTIMISM_MAINNET;BASE_MAINNET,POLYGON_MAINNET' + enabled: true + phaseTimeout: 20m + - name: 'BLAST_MAINNET' + pairs: 'BLAST_MAINNET,ARBITRUM_MAINNET;BLAST_MAINNET,BASE_MAINNET;BLAST_MAINNET,BSC_MAINNET' + enabled: true + phaseTimeout: 20m + - name: 'BSC_MAINNET' + pairs: 'BSC_MAINNET,OPTIMISM_MAINNET;BSC_MAINNET,POLYGON_MAINNET;BSC_MAINNET,WEMIX_MAINNET' + enabled: true + phaseTimeout: 20m + - name: 'ETHEREUM_MAINNET 1' + pairs: 'ETHEREUM_MAINNET,ARBITRUM_MAINNET;ETHEREUM_MAINNET,AVALANCHE_MAINNET;ETHEREUM_MAINNET,BASE_MAINNET;ETHEREUM_MAINNET,BLAST_MAINNET;ETHEREUM_MAINNET,BSC_MAINNET;ETHEREUM_MAINNET,CELO_MAINNET;ETHEREUM_MAINNET,GNOSIS_MAINNET;ETHEREUM_MAINNET,OPTIMISM_MAINNET;ETHEREUM_MAINNET,POLYGON_MAINNET;ETHEREUM_MAINNET,WEMIX_MAINNET' + enabled: true + phaseTimeout: 40m + - name: 'ETHEREUM_MAINNET 2' + pairs: 'ETHEREUM_MAINNET,METIS_ANDROMEDA;ETHEREUM_MAINNET,ZKSYNC_MAINNET' + enabled: true + phaseTimeout: 90m + - name: 'GNOSIS_MAINNET' + pairs: 'GNOSIS_MAINNET,ARBITRUM_MAINNET;GNOSIS_MAINNET,AVALANCHE_MAINNET;GNOSIS_MAINNET,BASE_MAINNET;GNOSIS_MAINNET,BSC_MAINNET;GNOSIS_MAINNET,OPTIMISM_MAINNET;GNOSIS_MAINNET,POLYGON_MAINNET' + enabled: true + phaseTimeout: 20m + - name: 'METIS_ANDROMEDA' + pairs: 'METIS_ANDROMEDA,ARBITRUM_MAINNET' + enabled: true + phaseTimeout: 60m + - name: 'MODE_MAINNET' + pairs: 'MODE_MAINNET,OPTIMISM_MAINNET;MODE_MAINNET,ARBITRUM_MAINNET;MODE_MAINNET,BASE_MAINNET;MODE_MAINNET,BSC_MAINNET' + enabled: true + phaseTimeout: 20m + - name: 'OPTIMISM_MAINNET' + pairs: 'OPTIMISM_MAINNET,POLYGON_MAINNET;OPTIMISM_MAINNET,WEMIX_MAINNET' + enabled: true + phaseTimeout: 20m + - name: 'POLYGON_MAINNET' + pairs: 'POLYGON_MAINNET,ARBITRUM_MAINNET;POLYGON_MAINNET,WEMIX_MAINNET' + enabled: true + phaseTimeout: 20m + - name: 'WEMIX_MAINNET' + pairs: 'WEMIX_MAINNET,ARBITRUM_MAINNET;WEMIX_MAINNET,KROMA_MAINNET' + enabled: true + phaseTimeout: 20m + - name: 'ZKSYNC_MAINNET' + pairs: 'ZKSYNC_MAINNET,ARBITRUM_MAINNET' + enabled: true + phaseTimeout: 90m steps: - name: Collect Metrics + if: ${{ matrix.lanes.enabled == true }} id: collect-gha-metrics uses: smartcontractkit/push-gha-metrics-action@dea9b546553cb4ca936607c2267a09c004e4ab3f # v3.0.0 with: @@ -261,10 +323,12 @@ jobs: this-job-name: CCIP Smoke Test continue-on-error: true - name: Checkout the repo + if: ${{ matrix.lanes.enabled == true }} uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: ref: ${{ env.REF_NAME }} - name: Prepare Base64 TOML override + if: ${{ matrix.lanes.enabled == true }} id: set_override_config shell: bash run: | @@ -276,14 +340,20 @@ jobs: echo ::add-mask::$BASE64_CCIP_CONFIG_OVERRIDE echo "base_64_override=$BASE64_CCIP_CONFIG_OVERRIDE" >> $GITHUB_OUTPUT fi + if [[ "${{ github.event_name }}" == "schedule" ]]; then + BASE64_CCIP_CONFIG_OVERRIDE=$(base64 -w 0 -i ./integration-tests/ccip-tests/testconfig/override/mainnet.toml) + echo ::add-mask::$BASE64_CCIP_CONFIG_OVERRIDE + echo "base_64_override=$BASE64_CCIP_CONFIG_OVERRIDE" >> $GITHUB_OUTPUT + echo "SLACK_USER=${{ secrets.QA_SLACK_USER }}" >> $GITHUB_ENV + fi - name: step summary + if: ${{ matrix.lanes.enabled == true }} shell: bash run: | echo "### chainlink image used for this test run :link:" >>$GITHUB_STEP_SUMMARY echo "\`${{ env.CHAINLINK_VERSION }}\`" >> $GITHUB_STEP_SUMMARY - echo "### chainlink-tests image tag for this test run :ship:" >>$GITHUB_STEP_SUMMARY - echo "\`${{ env.CHAINLINK_TEST_VERSION }}\`" >> $GITHUB_STEP_SUMMARY - name: Prepare Base64 TOML override for CCIP secrets + if: ${{ matrix.lanes.enabled == true }} uses: ./.github/actions/setup-create-base64-config-ccip id: setup_create_base64_config_ccip with: @@ -293,19 +363,22 @@ jobs: logstreamLogTargets: ${{ vars.LOGSTREAM_LOG_TARGETS }} - name: Run Tests uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/run-tests@94cb11f4bd545607a2f221c6685052b3abee723d # v2.3.32 + if: ${{ matrix.lanes.enabled == true }} env: TEST_SUITE: smoke TEST_ARGS: -test.timeout 900h - DETACH_RUNNER: true + DETACH_RUNNER: false DATABASE_URL: postgresql://postgres:node@localhost:5432/chainlink_test?sslmode=disable RR_MEM: 8Gi RR_CPU: 4 TEST_TRIGGERED_BY: ccip-smoke-test-ci - BASE64_CCIP_CONFIG_OVERRIDE: ${{ steps.set_override_config.outputs.base_64_override }},${{ steps.setup_create_base64_config_ccip.outputs.base64_config }} - TEST_BASE64_CCIP_CONFIG_OVERRIDE: ${{ steps.set_override_config.outputs.base_64_override }},${{ steps.setup_create_base64_config_ccip.outputs.base64_config }} + BASE64_CCIP_CONFIG_OVERRIDE: ${{ steps.setup_create_base64_config_ccip.outputs.base64_config }},${{ steps.set_override_config.outputs.base_64_override }} + TEST_BASE64_CCIP_CONFIG_OVERRIDE: ${{ steps.setup_create_base64_config_ccip.outputs.base64_config }},${{ steps.set_override_config.outputs.base_64_override }} E2E_TEST_GRAFANA_DASHBOARD_URL: "/d/ddf75041-1e39-42af-aa46-361fe4c36e9e/ci-e2e-tests-logs" + OVERRIDE_NETWORK_PAIRS: ${{ matrix.lanes.pairs }} + OVERRIDE_PHASE_TIMEOUT: ${{ matrix.lanes.phaseTimeout }} with: - test_command_to_run: cd ./integration-tests/ccip-tests && go test -v -timeout 70m -count=1 -p 30 -json -run ^TestSmokeCCIPForBidirectionalLane$ ./smoke 2>&1 | tee /tmp/gotest.log | gotestfmt + test_command_to_run: cd ./integration-tests/ccip-tests && go test -v -timeout 3h -count=1 -p 30 -json -run ^TestSmokeCCIPForGivenNetworkPairs$ ./smoke 2>&1 | tee /tmp/gotest.log | gotestloghelper -ci -singlepackage -hidepassingtests=false test_download_vendor_packages_command: cd ./integration-tests && go mod download # Other default test secrets loaded from dotenv Github Secret. test_secrets_defaults_base64: ${{ secrets.CCIP_DEFAULT_TEST_SECRETS }} @@ -314,7 +387,6 @@ jobs: go_mod_path: ./integration-tests/go.mod QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }} QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} - QA_KUBECONFIG: ${{ secrets.QA_KUBECONFIG }} triggered_by: ${{ env.TEST_TRIGGERED_BY }} artifacts_location: ./integration-tests/smoke/logs/payload_ccip.json aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} diff --git a/GNUmakefile b/GNUmakefile index 55d2de618e..5804d2c9d4 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -48,6 +48,10 @@ install-chainlink: operator-ui ## Install the chainlink binary. install-chainlink-cover: operator-ui ## Install the chainlink binary with cover flag. go install -cover $(GOFLAGS) . +.PHONY: install-chainlink-delve +install-chainlink-delve: operator-ui ## Install the chainlink binary. + go install $(GOFLAGS) -gcflags "all=-N -l" . + .PHONY: chainlink chainlink: ## Build the chainlink binary. go build $(GOFLAGS) . diff --git a/contracts/gas-snapshots/ccip.gas-snapshot b/contracts/gas-snapshots/ccip.gas-snapshot index e026dd1642..38e3a72dfc 100644 --- a/contracts/gas-snapshots/ccip.gas-snapshot +++ b/contracts/gas-snapshots/ccip.gas-snapshot @@ -811,6 +811,28 @@ PingPong_plumbing:test_OutOfOrderExecution_Success() (gas: 20310) PingPong_plumbing:test_Pausing_Success() (gas: 17810) PingPong_startPingPong:test_StartPingPong_With_OOO_Success() (gas: 162091) PingPong_startPingPong:test_StartPingPong_With_Sequenced_Ordered_Success() (gas: 181509) +RMNHome__validateStaticAndDynamicConfig:test_validateStaticAndDynamicConfig_DuplicateOffchainPublicKey_reverts() (gas: 18822) +RMNHome__validateStaticAndDynamicConfig:test_validateStaticAndDynamicConfig_DuplicatePeerId_reverts() (gas: 18682) +RMNHome__validateStaticAndDynamicConfig:test_validateStaticAndDynamicConfig_DuplicateSourceChain_reverts() (gas: 20371) +RMNHome__validateStaticAndDynamicConfig:test_validateStaticAndDynamicConfig_MinObserversTooHigh_reverts() (gas: 20810) +RMNHome__validateStaticAndDynamicConfig:test_validateStaticAndDynamicConfig_OutOfBoundsNodesLength_reverts() (gas: 137268) +RMNHome__validateStaticAndDynamicConfig:test_validateStaticAndDynamicConfig_OutOfBoundsObserverNodeIndex_reverts() (gas: 20472) +RMNHome_getConfigDigests:test_getConfigDigests_success() (gas: 1077745) +RMNHome_promoteCandidateAndRevokeActive:test_promoteCandidateAndRevokeActive_ConfigDigestMismatch_reverts() (gas: 23857) +RMNHome_promoteCandidateAndRevokeActive:test_promoteCandidateAndRevokeActive_NoOpStateTransitionNotAllowed_reverts() (gas: 10575) +RMNHome_promoteCandidateAndRevokeActive:test_promoteCandidateAndRevokeActive_OnlyOwner_reverts() (gas: 10936) +RMNHome_promoteCandidateAndRevokeActive:test_promoteCandidateAndRevokeActive_success() (gas: 1083071) +RMNHome_revokeCandidate:test_revokeCandidate_ConfigDigestMismatch_reverts() (gas: 19063) +RMNHome_revokeCandidate:test_revokeCandidate_OnlyOwner_reverts() (gas: 10963) +RMNHome_revokeCandidate:test_revokeCandidate_RevokingZeroDigestNotAllowed_reverts() (gas: 10606) +RMNHome_revokeCandidate:test_revokeCandidate_success() (gas: 28147) +RMNHome_setCandidate:test_setCandidate_ConfigDigestMismatch_reverts() (gas: 594679) +RMNHome_setCandidate:test_setCandidate_OnlyOwner_reverts() (gas: 15177) +RMNHome_setCandidate:test_setCandidate_success() (gas: 588379) +RMNHome_setDynamicConfig:test_setDynamicConfig_DigestNotFound_reverts() (gas: 30159) +RMNHome_setDynamicConfig:test_setDynamicConfig_MinObserversTooHigh_reverts() (gas: 18848) +RMNHome_setDynamicConfig:test_setDynamicConfig_OnlyOwner_reverts() (gas: 14115) +RMNHome_setDynamicConfig:test_setDynamicConfig_success() (gas: 103911) RMNRemote_constructor:test_constructor_success() (gas: 8334) RMNRemote_constructor:test_constructor_zeroChainSelector_reverts() (gas: 59165) RMNRemote_curse:test_curse_AlreadyCursed_duplicateSubject_reverts() (gas: 154457) diff --git a/contracts/src/v0.8/ccip/rmn/RMNHome.sol b/contracts/src/v0.8/ccip/rmn/RMNHome.sol index ec479bf663..0280daddc2 100644 --- a/contracts/src/v0.8/ccip/rmn/RMNHome.sol +++ b/contracts/src/v0.8/ccip/rmn/RMNHome.sol @@ -7,146 +7,384 @@ import {OwnerIsCreator} from "../../shared/access/OwnerIsCreator.sol"; /// @notice Stores the home configuration for RMN, that is referenced by CCIP oracles, RMN nodes, and the RMNRemote /// contracts. +/// @dev This contract is a state machine with the following states: +/// - Init: The initial state of the contract, no config has been set, or all configs have been revoked. +/// [0, 0] +/// +/// - Candidate: A new config has been set, but it has not been promoted yet, or all active configs have been revoked. +/// [0, 1] +/// +/// - Active: A non-zero config has been promoted and is active, there is no candidate configured. +/// [1, 0] +/// +/// - ActiveAndCandidate: A non-zero config has been promoted and is active, and a new config has been set as candidate. +/// [1, 1] +/// +/// The following state transitions are allowed: +/// - Init -> Candidate: setCandidate() +/// - Candidate -> Active: promoteCandidateAndRevokeActive() +/// - Candidate -> Candidate: setCandidate() +/// - Candidate -> Init: revokeCandidate() +/// - Active -> ActiveAndCandidate: setCandidate() +/// - Active -> Init: promoteCandidateAndRevokeActive() +/// - ActiveAndCandidate -> Active: promoteCandidateAndRevokeActive() +/// - ActiveAndCandidate -> Active: revokeCandidate() +/// - ActiveAndCandidate -> ActiveAndCandidate: setCandidate() +/// +/// This means the following calls are not allowed at the following states: +/// - Init: promoteCandidateAndRevokeActive(), as there is no config to promote. +/// - Init: revokeCandidate(), as there is no config to revoke +/// - Active: revokeCandidate(), as there is no candidate to revoke +/// Note that we explicitly do allow promoteCandidateAndRevokeActive() to be called when there is an active config but +/// no candidate config. This is the only way to remove the active config. The alternative would be to set some unusable +/// config as candidate and promote that, but fully clearing it is cleaner. +/// +/// ┌─────────────┐ setCandidate ┌─────────────┐ +/// │ ├───────────────────►│ │ setCandidate +/// │ Init │ revokeCandidate │ Candidate │◄───────────┐ +/// │ [0,0] │◄───────────────────┤ [0,1] │────────────┘ +/// │ │ ┌─────────────────┤ │ +/// └─────────────┘ │ promote- └─────────────┘ +/// ▲ │ Candidate +/// promote- │ │ +/// Candidate │ │ +/// │ │ +/// ┌──────────┴──┐ │ promote- ┌─────────────┐ +/// │ │◄─┘ Candidate OR │ Active & │ setCandidate +/// │ Active │ revokeCandidate │ Candidate │◄───────────┐ +/// │ [1,0] │◄───────────────────┤ [1,1] │────────────┘ +/// │ ├───────────────────►│ │ +/// └─────────────┘ setSecondary └─────────────┘ +/// contract RMNHome is OwnerIsCreator, ITypeAndVersion { + event ConfigSet(bytes32 indexed configDigest, uint32 version, StaticConfig staticConfig, DynamicConfig dynamicConfig); + event ActiveConfigRevoked(bytes32 indexed configDigest); + event CandidateConfigRevoked(bytes32 indexed configDigest); + event DynamicConfigSet(bytes32 indexed configDigest, DynamicConfig dynamicConfig); + event ConfigPromoted(bytes32 indexed configDigest); + + error OutOfBoundsNodesLength(); error DuplicatePeerId(); error DuplicateOffchainPublicKey(); - error OutOfOrderSourceChains(); - error OutOfOrderObserverNodeIndices(); + error DuplicateSourceChain(); error OutOfBoundsObserverNodeIndex(); error MinObserversTooHigh(); - - event ConfigSet(bytes32 configDigest, VersionedConfig versionedConfig); - event ConfigRevoked(bytes32 configDigest); + error ConfigDigestMismatch(bytes32 expectedConfigDigest, bytes32 gotConfigDigest); + error DigestNotFound(bytes32 configDigest); + error RevokingZeroDigestNotAllowed(); + error NoOpStateTransitionNotAllowed(); struct Node { - string peerId; // used for p2p communication, base58 encoded - bytes32 offchainPublicKey; // observations are signed with this public key, and are only verified offchain + bytes32 peerId; // Used for p2p communication. + bytes32 offchainPublicKey; // Observations are signed with this public key, and are only verified offchain. } struct SourceChain { - uint64 chainSelector; - uint64[] observerNodeIndices; // indices into Config.nodes, strictly increasing - uint64 minObservers; // required to agree on an observation for this source chain + uint64 chainSelector; // ─────╮ The Source chain selector. + uint64 minObservers; // ──────╯ Required number of observers to agree on an observation for this source chain. + // ObserverNodesBitmap & (1< 0 && !(newConfig.sourceChains[i - 1].chainSelector < newConfig.sourceChains[i].chainSelector)) { - revert OutOfOrderSourceChains(); - } + /// @notice Used for encoding the config digest prefix, unique per Home contract implementation. + uint256 private constant PREFIX = 0x000b << (256 - 16); // 0x000b00..00 + /// @notice Used for encoding the config digest prefix + uint256 private constant PREFIX_MASK = type(uint256).max << (256 - 16); // 0xFFFF00..00 + /// @notice The max number of configs that can be active at the same time. + uint256 private constant MAX_CONCURRENT_CONFIGS = 2; + /// @notice Helper to identify the zero config digest with less casting. + bytes32 private constant ZERO_DIGEST = bytes32(uint256(0)); + // @notice To ensure that observerNodesBitmap can be bit-encoded into a uint256. + uint256 private constant MAX_NODES = 256; - // all observerNodeIndices are valid - for (uint256 j = 0; j < newConfig.sourceChains[i].observerNodeIndices.length; ++j) { - if ( - j > 0 - && !(newConfig.sourceChains[i].observerNodeIndices[j - 1] < newConfig.sourceChains[i].observerNodeIndices[j]) - ) { - revert OutOfOrderObserverNodeIndices(); - } - if (!(newConfig.sourceChains[i].observerNodeIndices[j] < newConfig.nodes.length)) { - revert OutOfBoundsObserverNodeIndex(); - } - } + /// @notice This array holds the configs. + /// @dev Value i in this array is valid iff s_configs[i].configDigest != 0. + VersionedConfig[MAX_CONCURRENT_CONFIGS] private s_configs; - // minObservers are tenable - if (!(newConfig.sourceChains[i].minObservers <= newConfig.sourceChains[i].observerNodeIndices.length)) { - revert MinObserversTooHigh(); - } - } - } + /// @notice The latest version set, incremented by one for each new config. + uint32 private s_currentVersion = 0; + /// @notice The index of the active config. Used to determine which config is active. Adding the configs to a list + /// with two items and using this index to determine which one is active is a gas efficient way to handle this. Having + /// a set place for the active config would mean we have to copy the candidate config to the active config when it is + /// promoted, which would be more expensive. This index allows us to flip the configs around using `XOR 1`, which + /// flips 0 to 1 and 1 to 0. + uint32 private s_activeConfigIndex = 0; + + // ================================================================ + // │ Getters │ + // ================================================================ + + /// @notice Returns the current active and candidate config digests. + /// @dev Can be bytes32(0) if no config has been set yet or it has been revoked. + /// @return activeConfigDigest The digest of the active config. + /// @return candidateConfigDigest The digest of the candidate config. + function getConfigDigests() external view returns (bytes32 activeConfigDigest, bytes32 candidateConfigDigest) { + return (s_configs[getActiveIndex()].configDigest, s_configs[getCandidateIndex()].configDigest); + } + + /// @notice Returns the active config digest + function getActiveDigest() external view returns (bytes32) { + return s_configs[getActiveIndex()].configDigest; + } - uint256 oldConfigIndex = s_latestConfigIndex; - uint32 oldConfigCount = s_configCounts[oldConfigIndex]; - uint256 newConfigIndex = (oldConfigIndex + 1) % CONFIG_RING_BUFFER_SIZE; + /// @notice Returns the candidate config digest + function getCandidateDigest() public view returns (bytes32) { + return s_configs[getCandidateIndex()].configDigest; + } - for (uint256 i = 0; i < CONFIG_RING_BUFFER_SIZE; ++i) { - if ((i == newConfigIndex || revokePastConfigs) && s_configCounts[i] > 0) { - emit ConfigRevoked(_configDigest(VersionedConfig({version: s_configCounts[i], config: s_configs[i]}))); - delete s_configCounts[i]; + /// @notice The offchain code can use this to fetch an old config which might still be in use by some remotes. Use + /// in case one of the configs is too large to be returnable by one of the other getters. + /// @param configDigest The digest of the config to fetch. + /// @return versionedConfig The config and its version. + /// @return ok True if the config was found, false otherwise. + function getConfig(bytes32 configDigest) external view returns (VersionedConfig memory versionedConfig, bool ok) { + for (uint256 i = 0; i < MAX_CONCURRENT_CONFIGS; ++i) { + // We never want to return true for a zero digest, even if the caller is asking for it, as this can expose old + // config state that is invalid. + if (s_configs[i].configDigest == configDigest && configDigest != ZERO_DIGEST) { + return (s_configs[i], true); } } - - uint32 newConfigCount = oldConfigCount + 1; - VersionedConfig memory newVersionedConfig = VersionedConfig({version: newConfigCount, config: newConfig}); - bytes32 newConfigDigest = _configDigest(newVersionedConfig); - s_configs[newConfigIndex] = newConfig; - s_configCounts[newConfigIndex] = newConfigCount; - s_latestConfigIndex = newConfigIndex; - s_latestConfigDigest = newConfigDigest; - emit ConfigSet(newConfigDigest, newVersionedConfig); + return (versionedConfig, false); } - /// @return configDigest will be zero in case no config has been set - function getLatestConfigDigestAndVersionedConfig() + function getAllConfigs() external view - returns (bytes32 configDigest, VersionedConfig memory) + returns (VersionedConfig memory activeConfig, VersionedConfig memory candidateConfig) { - return ( - s_latestConfigDigest, - VersionedConfig({version: s_configCounts[s_latestConfigIndex], config: s_configs[s_latestConfigIndex]}) + VersionedConfig memory storedActiveConfig = s_configs[getActiveIndex()]; + if (storedActiveConfig.configDigest != ZERO_DIGEST) { + activeConfig = storedActiveConfig; + } + + VersionedConfig memory storedCandidateConfig = s_configs[getCandidateIndex()]; + if (storedCandidateConfig.configDigest != ZERO_DIGEST) { + candidateConfig = storedCandidateConfig; + } + + return (activeConfig, candidateConfig); + } + + // ================================================================ + // │ State transitions │ + // ================================================================ + + /// @notice Sets a new config as the candidate config. Does not influence the active config. + /// @param staticConfig The static part of the config. + /// @param dynamicConfig The dynamic part of the config. + /// @param digestToOverwrite The digest of the config to overwrite, or ZERO_DIGEST if no config is to be overwritten. + /// This is done to prevent accidental overwrites. + /// @return newConfigDigest The digest of the new config. + function setCandidate( + StaticConfig calldata staticConfig, + DynamicConfig calldata dynamicConfig, + bytes32 digestToOverwrite + ) external onlyOwner returns (bytes32 newConfigDigest) { + _validateStaticAndDynamicConfig(staticConfig, dynamicConfig); + + bytes32 existingDigest = getCandidateDigest(); + + if (existingDigest != digestToOverwrite) { + revert ConfigDigestMismatch(existingDigest, digestToOverwrite); + } + + // are we going to overwrite a config? If so, emit an event. + if (existingDigest != ZERO_DIGEST) { + emit CandidateConfigRevoked(digestToOverwrite); + } + + uint32 newVersion = ++s_currentVersion; + newConfigDigest = _calculateConfigDigest(abi.encode(staticConfig), newVersion); + + VersionedConfig storage existingConfig = s_configs[getCandidateIndex()]; + existingConfig.configDigest = newConfigDigest; + existingConfig.version = newVersion; + existingConfig.staticConfig = staticConfig; + existingConfig.dynamicConfig = dynamicConfig; + + emit ConfigSet(newConfigDigest, newVersion, staticConfig, dynamicConfig); + + return newConfigDigest; + } + + /// @notice Revokes a specific config by digest. This is used when the candidate config turns out to be incorrect to + /// remove it without it ever having to be promoted. It's also possible to revoke the candidate config by setting a + /// newer candidate config using `setCandidate`. + /// @param configDigest The digest of the config to revoke. This is done to prevent accidental revokes. + function revokeCandidate(bytes32 configDigest) external onlyOwner { + if (configDigest == ZERO_DIGEST) { + revert RevokingZeroDigestNotAllowed(); + } + + uint256 candidateConfigIndex = getCandidateIndex(); + if (s_configs[candidateConfigIndex].configDigest != configDigest) { + revert ConfigDigestMismatch(s_configs[candidateConfigIndex].configDigest, configDigest); + } + + emit CandidateConfigRevoked(configDigest); + // Delete only the digest, as that's what's used to determine if a config is active. This means the actual + // config stays in storage which should significantly reduce the gas cost of overwriting that storage space in + // the future. + delete s_configs[candidateConfigIndex].configDigest; + } + + /// @notice Promotes the candidate config to the active config and revokes the active config. + /// @param digestToPromote The digest of the config to promote. + /// @param digestToRevoke The digest of the config to revoke. + /// @dev No config is changed in storage, the only storage changes that happen are + /// - The activeConfigIndex is flipped. + /// - The digest of the old active config is deleted. + function promoteCandidateAndRevokeActive(bytes32 digestToPromote, bytes32 digestToRevoke) external onlyOwner { + if (digestToPromote == ZERO_DIGEST && digestToRevoke == ZERO_DIGEST) { + revert NoOpStateTransitionNotAllowed(); + } + + uint256 candidateConfigIndex = getCandidateIndex(); + if (s_configs[candidateConfigIndex].configDigest != digestToPromote) { + revert ConfigDigestMismatch(s_configs[candidateConfigIndex].configDigest, digestToPromote); + } + + VersionedConfig storage activeConfig = s_configs[getActiveIndex()]; + if (activeConfig.configDigest != digestToRevoke) { + revert ConfigDigestMismatch(activeConfig.configDigest, digestToRevoke); + } + + delete activeConfig.configDigest; + + s_activeConfigIndex ^= 1; + if (digestToRevoke != ZERO_DIGEST) { + emit ActiveConfigRevoked(digestToRevoke); + } + emit ConfigPromoted(digestToPromote); + } + + /// @notice Sets the dynamic config for a specific config. + /// @param newDynamicConfig The new dynamic config. + /// @param currentDigest The digest of the config to update. + /// @dev This does not update the config digest as only the static config is part of the digest. + function setDynamicConfig(DynamicConfig calldata newDynamicConfig, bytes32 currentDigest) external onlyOwner { + for (uint256 i = 0; i < MAX_CONCURRENT_CONFIGS; ++i) { + if (s_configs[i].configDigest == currentDigest && currentDigest != ZERO_DIGEST) { + _validateDynamicConfig(newDynamicConfig, s_configs[i].staticConfig.nodes.length); + // Since the static config doesn't change we don't have to update the digest or version. + s_configs[i].dynamicConfig = newDynamicConfig; + + emit DynamicConfigSet(currentDigest, newDynamicConfig); + return; + } + } + + revert DigestNotFound(currentDigest); + } + + /// @notice Calculates the config digest for a given plugin key, static config, and version. + /// @param staticConfig The static part of the config. + /// @param version The version of the config. + /// @return The calculated config digest. + function _calculateConfigDigest(bytes memory staticConfig, uint32 version) internal view returns (bytes32) { + return bytes32( + (PREFIX & PREFIX_MASK) + | ( + uint256( + keccak256(bytes.concat(abi.encode(bytes32("EVM"), block.chainid, address(this), version), staticConfig)) + ) & ~PREFIX_MASK + ) ); } - /// @notice The offchain code can use this to fetch an old config which might still be in use by some remotes - /// @dev Only to be called by offchain code, efficiency is not a concern - function getConfig(bytes32 configDigest) external view returns (VersionedConfig memory versionedConfig, bool ok) { - for (uint256 i = 0; i < CONFIG_RING_BUFFER_SIZE; ++i) { - if (s_configCounts[i] == 0) { - // unset config - continue; + function getActiveIndex() private view returns (uint32) { + return s_activeConfigIndex; + } + + function getCandidateIndex() private view returns (uint32) { + return s_activeConfigIndex ^ 1; + } + + // ================================================================ + // │ Validation │ + // ================================================================ + + /// @notice Validates the static and dynamic config. Reverts when the config is invalid. + /// @param staticConfig The static part of the config. + /// @param dynamicConfig The dynamic part of the config. + function _validateStaticAndDynamicConfig( + StaticConfig memory staticConfig, + DynamicConfig memory dynamicConfig + ) internal pure { + // Ensure that observerNodesBitmap can be bit-encoded into a uint256. + if (staticConfig.nodes.length > MAX_NODES) { + revert OutOfBoundsNodesLength(); + } + + // Ensure no peerId or offchainPublicKey is duplicated. + for (uint256 i = 0; i < staticConfig.nodes.length; ++i) { + for (uint256 j = i + 1; j < staticConfig.nodes.length; ++j) { + if (staticConfig.nodes[i].peerId == staticConfig.nodes[j].peerId) { + revert DuplicatePeerId(); + } + if (staticConfig.nodes[i].offchainPublicKey == staticConfig.nodes[j].offchainPublicKey) { + revert DuplicateOffchainPublicKey(); + } + } + } + + _validateDynamicConfig(dynamicConfig, staticConfig.nodes.length); + } + + /// @notice Validates the dynamic config. Reverts when the config is invalid. + /// @param dynamicConfig The dynamic part of the config. + /// @param numberOfNodes The number of nodes in the static config. + function _validateDynamicConfig(DynamicConfig memory dynamicConfig, uint256 numberOfNodes) internal pure { + uint256 numberOfSourceChains = dynamicConfig.sourceChains.length; + for (uint256 i = 0; i < numberOfSourceChains; ++i) { + SourceChain memory currentSourceChain = dynamicConfig.sourceChains[i]; + // Ensure the source chain is unique. + for (uint256 j = i + 1; j < numberOfSourceChains; ++j) { + if (currentSourceChain.chainSelector == dynamicConfig.sourceChains[j].chainSelector) { + revert DuplicateSourceChain(); + } } - VersionedConfig memory vc = VersionedConfig({version: s_configCounts[i], config: s_configs[i]}); - if (_configDigest(vc) == configDigest) { - versionedConfig = vc; - ok = true; - break; + + // all observer node indices are valid + uint256 bitmap = currentSourceChain.observerNodesBitmap; + // Check if there are any bits set for indexes outside of the expected range. + if (bitmap & (type(uint256).max >> (256 - numberOfNodes)) != bitmap) { + revert OutOfBoundsObserverNodeIndex(); + } + + uint256 observersCount = 0; + for (; bitmap != 0; ++observersCount) { + bitmap &= bitmap - 1; + } + + // minObservers are tenable + if (currentSourceChain.minObservers > observersCount) { + revert MinObserversTooHigh(); } } } diff --git a/contracts/src/v0.8/ccip/test/rmn/RMNHome.t.sol b/contracts/src/v0.8/ccip/test/rmn/RMNHome.t.sol index 40cb1e4a07..c5226d3206 100644 --- a/contracts/src/v0.8/ccip/test/rmn/RMNHome.t.sol +++ b/contracts/src/v0.8/ccip/test/rmn/RMNHome.t.sol @@ -3,13 +3,375 @@ pragma solidity 0.8.24; import {Internal} from "../../libraries/Internal.sol"; import {RMNHome} from "../../rmn/RMNHome.sol"; -import {BaseTest} from "../BaseTest.t.sol"; +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; -contract RMNHomeTest is BaseTest { - RMNHome public s_rmnHome; +contract RMNHomeTest is Test { + struct Config { + RMNHome.StaticConfig staticConfig; + RMNHome.DynamicConfig dynamicConfig; + } + + bytes32 internal constant ZERO_DIGEST = bytes32(uint256(0)); + RMNHome public s_rmnHome = new RMNHome(); + + function _getBaseConfig() internal pure returns (Config memory) { + RMNHome.Node[] memory nodes = new RMNHome.Node[](3); + nodes[0] = RMNHome.Node({peerId: keccak256("peerId_0"), offchainPublicKey: keccak256("offchainPublicKey_0")}); + nodes[1] = RMNHome.Node({peerId: keccak256("peerId_1"), offchainPublicKey: keccak256("offchainPublicKey_1")}); + nodes[2] = RMNHome.Node({peerId: keccak256("peerId_2"), offchainPublicKey: keccak256("offchainPublicKey_2")}); + + RMNHome.SourceChain[] memory sourceChains = new RMNHome.SourceChain[](2); + // Observer 0 for source chain 9000 + sourceChains[0] = RMNHome.SourceChain({chainSelector: 9000, minObservers: 1, observerNodesBitmap: 1 << 0}); + // Observers 1 and 2 for source chain 9001 + sourceChains[1] = RMNHome.SourceChain({chainSelector: 9001, minObservers: 2, observerNodesBitmap: 1 << 1 | 1 << 2}); + + return Config({ + staticConfig: RMNHome.StaticConfig({nodes: nodes, offchainConfig: abi.encode("static_config")}), + dynamicConfig: RMNHome.DynamicConfig({sourceChains: sourceChains, offchainConfig: abi.encode("dynamic_config")}) + }); + } + + uint256 private constant PREFIX_MASK = type(uint256).max << (256 - 16); // 0xFFFF00..00 + uint256 private constant PREFIX = 0x000b << (256 - 16); // 0x000b00..00 + + function _getConfigDigest(bytes memory staticConfig, uint32 version) internal view returns (bytes32) { + return bytes32( + (PREFIX & PREFIX_MASK) + | ( + uint256( + keccak256(bytes.concat(abi.encode(bytes32("EVM"), block.chainid, address(s_rmnHome), version), staticConfig)) + ) & ~PREFIX_MASK + ) + ); + } +} + +contract RMNHome_getConfigDigests is RMNHomeTest { + function test_getConfigDigests_success() public { + (bytes32 activeDigest, bytes32 candidateDigest) = s_rmnHome.getConfigDigests(); + assertEq(activeDigest, ZERO_DIGEST); + assertEq(candidateDigest, ZERO_DIGEST); + + Config memory config = _getBaseConfig(); + bytes32 firstDigest = s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + + (activeDigest, candidateDigest) = s_rmnHome.getConfigDigests(); + assertEq(activeDigest, ZERO_DIGEST); + assertEq(candidateDigest, firstDigest); + + s_rmnHome.promoteCandidateAndRevokeActive(firstDigest, ZERO_DIGEST); + + (activeDigest, candidateDigest) = s_rmnHome.getConfigDigests(); + assertEq(activeDigest, firstDigest); + assertEq(candidateDigest, ZERO_DIGEST); + + bytes32 secondDigest = s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + + (activeDigest, candidateDigest) = s_rmnHome.getConfigDigests(); + assertEq(activeDigest, firstDigest); + assertEq(candidateDigest, secondDigest); + + assertEq(activeDigest, s_rmnHome.getActiveDigest()); + assertEq(candidateDigest, s_rmnHome.getCandidateDigest()); + } +} + +contract RMNHome_setCandidate is RMNHomeTest { + function test_setCandidate_success() public { + Config memory config = _getBaseConfig(); + RMNHome.VersionedConfig memory versionedConfig = RMNHome.VersionedConfig({ + version: 1, + staticConfig: config.staticConfig, + dynamicConfig: config.dynamicConfig, + configDigest: ZERO_DIGEST + }); + + versionedConfig.configDigest = _getConfigDigest(abi.encode(versionedConfig.staticConfig), versionedConfig.version); + + vm.expectEmit(); + emit RMNHome.ConfigSet( + versionedConfig.configDigest, versionedConfig.version, versionedConfig.staticConfig, versionedConfig.dynamicConfig + ); + + s_rmnHome.setCandidate(versionedConfig.staticConfig, versionedConfig.dynamicConfig, ZERO_DIGEST); + + (RMNHome.VersionedConfig memory storedVersionedConfig, bool ok) = s_rmnHome.getConfig(versionedConfig.configDigest); + assertTrue(ok); + assertEq(storedVersionedConfig.version, versionedConfig.version); + RMNHome.StaticConfig memory storedStaticConfig = storedVersionedConfig.staticConfig; + RMNHome.DynamicConfig memory storedDynamicConfig = storedVersionedConfig.dynamicConfig; + + assertEq(storedStaticConfig.nodes.length, versionedConfig.staticConfig.nodes.length); + for (uint256 i = 0; i < storedStaticConfig.nodes.length; i++) { + RMNHome.Node memory storedNode = storedStaticConfig.nodes[i]; + assertEq(storedNode.peerId, versionedConfig.staticConfig.nodes[i].peerId); + assertEq(storedNode.offchainPublicKey, versionedConfig.staticConfig.nodes[i].offchainPublicKey); + } + + assertEq(storedDynamicConfig.sourceChains.length, versionedConfig.dynamicConfig.sourceChains.length); + for (uint256 i = 0; i < storedDynamicConfig.sourceChains.length; i++) { + RMNHome.SourceChain memory storedSourceChain = storedDynamicConfig.sourceChains[i]; + assertEq(storedSourceChain.chainSelector, versionedConfig.dynamicConfig.sourceChains[i].chainSelector); + assertEq(storedSourceChain.minObservers, versionedConfig.dynamicConfig.sourceChains[i].minObservers); + assertEq(storedSourceChain.observerNodesBitmap, versionedConfig.dynamicConfig.sourceChains[i].observerNodesBitmap); + } + assertEq(storedDynamicConfig.offchainConfig, versionedConfig.dynamicConfig.offchainConfig); + assertEq(storedStaticConfig.offchainConfig, versionedConfig.staticConfig.offchainConfig); + } + + function test_setCandidate_ConfigDigestMismatch_reverts() public { + Config memory config = _getBaseConfig(); + + bytes32 digest = s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + + vm.expectRevert(abi.encodeWithSelector(RMNHome.ConfigDigestMismatch.selector, digest, ZERO_DIGEST)); + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + + vm.expectEmit(); + emit RMNHome.CandidateConfigRevoked(digest); + + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, digest); + } + + function test_setCandidate_OnlyOwner_reverts() public { + Config memory config = _getBaseConfig(); + + vm.startPrank(address(0)); + + vm.expectRevert("Only callable by owner"); + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + } +} + +contract RMNHome_revokeCandidate is RMNHomeTest { + // Sets two configs + function setUp() public { + Config memory config = _getBaseConfig(); + bytes32 digest = s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + s_rmnHome.promoteCandidateAndRevokeActive(digest, ZERO_DIGEST); + + config.dynamicConfig.sourceChains[0].minObservers--; + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + } + + function test_revokeCandidate_success() public { + (bytes32 priorActiveDigest, bytes32 priorCandidateDigest) = s_rmnHome.getConfigDigests(); + + vm.expectEmit(); + emit RMNHome.CandidateConfigRevoked(priorCandidateDigest); + + s_rmnHome.revokeCandidate(priorCandidateDigest); + + (RMNHome.VersionedConfig memory storedVersionedConfig, bool ok) = s_rmnHome.getConfig(priorCandidateDigest); + assertFalse(ok); + // Ensure no old data is returned, even though it's still in storage + assertEq(storedVersionedConfig.version, 0); + assertEq(storedVersionedConfig.staticConfig.nodes.length, 0); + assertEq(storedVersionedConfig.dynamicConfig.sourceChains.length, 0); + + // Asser the active digest is unaffected but the candidate digest is set to zero + (bytes32 activeDigest, bytes32 candidateDigest) = s_rmnHome.getConfigDigests(); + assertEq(activeDigest, priorActiveDigest); + assertEq(candidateDigest, ZERO_DIGEST); + assertTrue(candidateDigest != priorCandidateDigest); + } + + function test_revokeCandidate_ConfigDigestMismatch_reverts() public { + (, bytes32 priorCandidateDigest) = s_rmnHome.getConfigDigests(); + + bytes32 wrongDigest = keccak256("wrong_digest"); + vm.expectRevert(abi.encodeWithSelector(RMNHome.ConfigDigestMismatch.selector, priorCandidateDigest, wrongDigest)); + s_rmnHome.revokeCandidate(wrongDigest); + } + + function test_revokeCandidate_RevokingZeroDigestNotAllowed_reverts() public { + vm.expectRevert(RMNHome.RevokingZeroDigestNotAllowed.selector); + s_rmnHome.revokeCandidate(ZERO_DIGEST); + } + + function test_revokeCandidate_OnlyOwner_reverts() public { + vm.startPrank(address(0)); + + vm.expectRevert("Only callable by owner"); + s_rmnHome.revokeCandidate(keccak256("configDigest")); + } +} + +contract RMNHome_promoteCandidateAndRevokeActive is RMNHomeTest { + function test_promoteCandidateAndRevokeActive_success() public { + Config memory config = _getBaseConfig(); + bytes32 firstConfigToPromote = s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + + vm.expectEmit(); + emit RMNHome.ConfigPromoted(firstConfigToPromote); + + s_rmnHome.promoteCandidateAndRevokeActive(firstConfigToPromote, ZERO_DIGEST); + + // Assert the active digest is updated and the candidate digest is set to zero + (bytes32 activeDigest, bytes32 candidateDigest) = s_rmnHome.getConfigDigests(); + assertEq(activeDigest, firstConfigToPromote); + assertEq(candidateDigest, ZERO_DIGEST); + + // Set a new candidate to promote over a non-zero active config. + config.staticConfig.offchainConfig = abi.encode("new_static_config"); + config.dynamicConfig.offchainConfig = abi.encode("new_dynamic_config"); + bytes32 secondConfigToPromote = s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + + vm.expectEmit(); + emit RMNHome.ActiveConfigRevoked(firstConfigToPromote); + + vm.expectEmit(); + emit RMNHome.ConfigPromoted(secondConfigToPromote); + + s_rmnHome.promoteCandidateAndRevokeActive(secondConfigToPromote, firstConfigToPromote); + + (RMNHome.VersionedConfig memory activeConfig, RMNHome.VersionedConfig memory candidateConfig) = + s_rmnHome.getAllConfigs(); + assertEq(activeConfig.configDigest, secondConfigToPromote); + assertEq(activeConfig.staticConfig.offchainConfig, config.staticConfig.offchainConfig); + assertEq(activeConfig.dynamicConfig.offchainConfig, config.dynamicConfig.offchainConfig); + + assertEq(candidateConfig.configDigest, ZERO_DIGEST); + } + + function test_promoteCandidateAndRevokeActive_NoOpStateTransitionNotAllowed_reverts() public { + vm.expectRevert(RMNHome.NoOpStateTransitionNotAllowed.selector); + s_rmnHome.promoteCandidateAndRevokeActive(ZERO_DIGEST, ZERO_DIGEST); + } + + function test_promoteCandidateAndRevokeActive_ConfigDigestMismatch_reverts() public { + (bytes32 priorActiveDigest, bytes32 priorCandidateDigest) = s_rmnHome.getConfigDigests(); + bytes32 wrongActiveDigest = keccak256("wrongActiveDigest"); + bytes32 wrongCandidateDigest = keccak256("wrongCandidateDigest"); + + vm.expectRevert( + abi.encodeWithSelector(RMNHome.ConfigDigestMismatch.selector, priorActiveDigest, wrongCandidateDigest) + ); + s_rmnHome.promoteCandidateAndRevokeActive(wrongCandidateDigest, wrongActiveDigest); + + vm.expectRevert(abi.encodeWithSelector(RMNHome.ConfigDigestMismatch.selector, priorActiveDigest, wrongActiveDigest)); + + s_rmnHome.promoteCandidateAndRevokeActive(priorCandidateDigest, wrongActiveDigest); + } + + function test_promoteCandidateAndRevokeActive_OnlyOwner_reverts() public { + vm.startPrank(address(0)); + + vm.expectRevert("Only callable by owner"); + s_rmnHome.promoteCandidateAndRevokeActive(keccak256("toPromote"), keccak256("ToRevoke")); + } +} + +contract RMNHome__validateStaticAndDynamicConfig is RMNHomeTest { + function test_validateStaticAndDynamicConfig_OutOfBoundsNodesLength_reverts() public { + Config memory config = _getBaseConfig(); + config.staticConfig.nodes = new RMNHome.Node[](257); + + vm.expectRevert(RMNHome.OutOfBoundsNodesLength.selector); + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + } + + function test_validateStaticAndDynamicConfig_DuplicatePeerId_reverts() public { + Config memory config = _getBaseConfig(); + config.staticConfig.nodes[1].peerId = config.staticConfig.nodes[0].peerId; + + vm.expectRevert(RMNHome.DuplicatePeerId.selector); + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + } + + function test_validateStaticAndDynamicConfig_DuplicateOffchainPublicKey_reverts() public { + Config memory config = _getBaseConfig(); + config.staticConfig.nodes[1].offchainPublicKey = config.staticConfig.nodes[0].offchainPublicKey; + + vm.expectRevert(RMNHome.DuplicateOffchainPublicKey.selector); + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + } + + function test_validateStaticAndDynamicConfig_DuplicateSourceChain_reverts() public { + Config memory config = _getBaseConfig(); + config.dynamicConfig.sourceChains[1].chainSelector = config.dynamicConfig.sourceChains[0].chainSelector; + + vm.expectRevert(RMNHome.DuplicateSourceChain.selector); + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + } + + function test_validateStaticAndDynamicConfig_OutOfBoundsObserverNodeIndex_reverts() public { + Config memory config = _getBaseConfig(); + config.dynamicConfig.sourceChains[0].observerNodesBitmap = 1 << config.staticConfig.nodes.length; + + vm.expectRevert(RMNHome.OutOfBoundsObserverNodeIndex.selector); + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + } + + function test_validateStaticAndDynamicConfig_MinObserversTooHigh_reverts() public { + Config memory config = _getBaseConfig(); + config.dynamicConfig.sourceChains[0].minObservers++; + + vm.expectRevert(RMNHome.MinObserversTooHigh.selector); + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + } +} + +contract RMNHome_setDynamicConfig is RMNHomeTest { + function setUp() public { + Config memory config = _getBaseConfig(); + s_rmnHome.setCandidate(config.staticConfig, config.dynamicConfig, ZERO_DIGEST); + } + + function test_setDynamicConfig_success() public { + (bytes32 priorActiveDigest,) = s_rmnHome.getConfigDigests(); + + Config memory config = _getBaseConfig(); + config.dynamicConfig.sourceChains[0].minObservers--; + + (, bytes32 candidateConfigDigest) = s_rmnHome.getConfigDigests(); + + vm.expectEmit(); + emit RMNHome.DynamicConfigSet(candidateConfigDigest, config.dynamicConfig); + + s_rmnHome.setDynamicConfig(config.dynamicConfig, candidateConfigDigest); + + (RMNHome.VersionedConfig memory storedVersionedConfig, bool ok) = s_rmnHome.getConfig(candidateConfigDigest); + assertTrue(ok); + assertEq( + storedVersionedConfig.dynamicConfig.sourceChains[0].minObservers, + config.dynamicConfig.sourceChains[0].minObservers + ); + + // Asser the digests don't change when updating the dynamic config + (bytes32 activeDigest, bytes32 candidateDigest) = s_rmnHome.getConfigDigests(); + assertEq(activeDigest, priorActiveDigest); + assertEq(candidateDigest, candidateConfigDigest); + } + + // Asserts the validation function is being called + function test_setDynamicConfig_MinObserversTooHigh_reverts() public { + Config memory config = _getBaseConfig(); + config.dynamicConfig.sourceChains[0].minObservers++; + + vm.expectRevert(abi.encodeWithSelector(RMNHome.DigestNotFound.selector, ZERO_DIGEST)); + s_rmnHome.setDynamicConfig(config.dynamicConfig, ZERO_DIGEST); + } + + function test_setDynamicConfig_DigestNotFound_reverts() public { + // Zero always reverts + vm.expectRevert(abi.encodeWithSelector(RMNHome.DigestNotFound.selector, ZERO_DIGEST)); + s_rmnHome.setDynamicConfig(_getBaseConfig().dynamicConfig, ZERO_DIGEST); + + // Non-existent digest reverts + bytes32 nonExistentDigest = keccak256("nonExistentDigest"); + vm.expectRevert(abi.encodeWithSelector(RMNHome.DigestNotFound.selector, nonExistentDigest)); + s_rmnHome.setDynamicConfig(_getBaseConfig().dynamicConfig, nonExistentDigest); + } + + function test_setDynamicConfig_OnlyOwner_reverts() public { + Config memory config = _getBaseConfig(); + + vm.startPrank(address(0)); - function setUp() public virtual override { - super.setUp(); - s_rmnHome = new RMNHome(); + vm.expectRevert("Only callable by owner"); + s_rmnHome.setDynamicConfig(config.dynamicConfig, keccak256("configDigest")); } } diff --git a/core/chainlink.debug.Dockerfile b/core/chainlink.debug.Dockerfile new file mode 100644 index 0000000000..2e9229cd4c --- /dev/null +++ b/core/chainlink.debug.Dockerfile @@ -0,0 +1,97 @@ +# Build image: Chainlink binary +FROM golang:1.22-bullseye as buildgo +RUN go version +WORKDIR /chainlink + +COPY GNUmakefile package.json ./ +COPY tools/bin/ldflags ./tools/bin/ + +ADD go.mod go.sum ./ +RUN go mod download + +# Env vars needed for chainlink build +ARG COMMIT_SHA + +# Build chainlink bin with cover flag https://go.dev/doc/build-cover#FAQ +ARG GO_COVER_FLAG=false + +COPY . . + +RUN apt-get update && apt-get install -y jq + +# Build the golang binary +RUN if [ "$GO_COVER_FLAG" = "true" ]; then \ + make install-chainlink-cover; \ + else \ + make install-chainlink-delve; \ + fi + +# Link LOOP Plugin source dirs with simple names +RUN go list -m -f "{{.Dir}}" github.com/smartcontractkit/chainlink-feeds | xargs -I % ln -s % /chainlink-feeds +RUN go list -m -f "{{.Dir}}" github.com/smartcontractkit/chainlink-solana | xargs -I % ln -s % /chainlink-solana + +# Build image: Plugins +FROM golang:1.22-bullseye as buildplugins +RUN go version + +WORKDIR /chainlink-feeds +COPY --from=buildgo /chainlink-feeds . +RUN go install ./cmd/chainlink-feeds + +WORKDIR /chainlink-solana +COPY --from=buildgo /chainlink-solana . +RUN go install ./pkg/solana/cmd/chainlink-solana + +# Final image: ubuntu with chainlink binary +FROM golang:1.22-bullseye + +ARG CHAINLINK_USER=root +ENV DEBIAN_FRONTEND noninteractive +RUN apt-get update && apt-get install -y ca-certificates gnupg lsb-release curl + +# Install Postgres for CLI tools, needed specifically for DB backups +RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \ + && echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |tee /etc/apt/sources.list.d/pgdg.list \ + && apt-get update && apt-get install -y postgresql-client-16 \ + && apt-get clean all \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=buildgo /go/bin/chainlink /usr/local/bin/ + +# Install (but don't enable) LOOP Plugins +COPY --from=buildplugins /go/bin/chainlink-feeds /usr/local/bin/ +COPY --from=buildplugins /go/bin/chainlink-solana /usr/local/bin/ + +# Dependency of CosmWasm/wasmd +COPY --from=buildgo /go/pkg/mod/github.com/\!cosm\!wasm/wasmvm@v*/internal/api/libwasmvm.*.so /usr/lib/ +RUN chmod 755 /usr/lib/libwasmvm.*.so + +RUN if [ ${CHAINLINK_USER} != root ]; then \ + useradd --uid 14933 --create-home ${CHAINLINK_USER}; \ + fi +USER ${CHAINLINK_USER} +WORKDIR /home/${CHAINLINK_USER} +RUN mkdir -p go +# explicit set the cache dir. needed so both root and non-root user has an explicit location +ENV XDG_CACHE_HOME /home/${CHAINLINK_USER}/.cache +RUN mkdir -p ${XDG_CACHE_HOME} + +# Set up env and dir for go coverage profiling https://go.dev/doc/build-cover#FAQ +ARG GO_COVER_DIR="/var/tmp/go-coverage" +ENV GOCOVERDIR=${GO_COVER_DIR} +RUN mkdir -p $GO_COVER_DIR + +# Install dlv +ENV GOPATH=/home/${CHAINLINK_USER}/go +ENV PATH=$PATH:$GOPATH/bin +RUN go install github.com/go-delve/delve/cmd/dlv@latest + +EXPOSE 6688 +ENTRYPOINT ["chainlink"] + +HEALTHCHECK CMD curl -f http://localhost:6688/health || exit 1 + +# Delve port +EXPOSE 40000 + +CMD ["dlv", "exec", "/usr/local/bin/chainlink", "--accept-multiclient", "--headless", "--listen=0.0.0.0:40000", "--api-version=2", "--", "local", "node"] diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index 7c99083a97..699c552030 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -7,23 +7,20 @@ import ( "fmt" "log" "strconv" + "strings" "time" - "gopkg.in/guregu/null.v4" - - cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" - "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "gopkg.in/guregu/null.v4" + chainselectors "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/libocr/commontypes" libocr2 "github.com/smartcontractkit/libocr/offchainreporting2plus" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "google.golang.org/grpc" ocr2keepers20 "github.com/smartcontractkit/chainlink-automation/pkg/v2" ocr2keepers20config "github.com/smartcontractkit/chainlink-automation/pkg/v2/config" @@ -38,10 +35,11 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins/ocr3" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" "github.com/smartcontractkit/chainlink-common/pkg/types" + cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" datastreamsllo "github.com/smartcontractkit/chainlink-data-streams/llo" - "github.com/smartcontractkit/chainlink/v2/core/bridges" "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" coreconfig "github.com/smartcontractkit/chainlink/v2/core/config" @@ -52,8 +50,11 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" "github.com/smartcontractkit/chainlink/v2/core/services/llo" + "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" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" + ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/functions" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/generic" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager" @@ -63,7 +64,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/median" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/mercury" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ocr2keeper" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/autotelemetry21" ocr2keeper21core "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/core" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" @@ -79,8 +79,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/synchronization" "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" "github.com/smartcontractkit/chainlink/v2/plugins" - - ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" ) type ErrJobSpecNoRelayer struct { @@ -1618,7 +1616,77 @@ func (d *Delegate) newServicesCCIPCommit(ctx context.Context, lggr logger.Sugare MetricsRegisterer: prometheus.WrapRegistererWith(map[string]string{"job_name": jb.Name.ValueOrZero()}, prometheus.DefaultRegisterer), } - return ccipcommit.NewCommitServices(ctx, d.ds, srcProvider, dstProvider, d.legacyChains, jb, lggr, d.pipelineRunner, oracleArgsNoPlugin, d.isNewlyCreatedJob, int64(srcChainID), dstChainID, logError) + var priceGetter ccip.AllTokensPriceGetter + withPipeline := strings.Trim(pluginJobSpecConfig.TokenPricesUSDPipeline, "\n\t ") != "" + if withPipeline { + priceGetter, err = ccip.NewPipelineGetter(pluginJobSpecConfig.TokenPricesUSDPipeline, d.pipelineRunner, jb.ID, jb.ExternalJobID, jb.Name.ValueOrZero(), lggr) + if err != nil { + return nil, fmt.Errorf("creating pipeline price getter: %w", err) + } + } else { + // Use dynamic price getter. + if pluginJobSpecConfig.PriceGetterConfig == nil { + return nil, fmt.Errorf("priceGetterConfig is nil") + } + + // Configure contract readers for all chains specified in the aggregator configurations. + // Some lanes (e.g. Wemix/Kroma) requires other clients than source and destination, since they use feeds from other chains. + aggregatorChainsToContracts := make(map[uint64][]common.Address) + for _, aggCfg := range pluginJobSpecConfig.PriceGetterConfig.AggregatorPrices { + if _, ok := aggregatorChainsToContracts[aggCfg.ChainID]; !ok { + aggregatorChainsToContracts[aggCfg.ChainID] = make([]common.Address, 0) + } + + aggregatorChainsToContracts[aggCfg.ChainID] = append(aggregatorChainsToContracts[aggCfg.ChainID], aggCfg.AggregatorContractAddress) + } + + contractReaders := map[uint64]types.ContractReader{} + + for chainID, aggregatorContracts := range aggregatorChainsToContracts { + relayID := types.RelayID{Network: spec.Relay, ChainID: strconv.FormatUint(chainID, 10)} + relay, rerr := d.RelayGetter.Get(relayID) + if rerr != nil { + return nil, fmt.Errorf("get relay by id=%v: %w", relayID, err) + } + + contractsConfig := make(map[string]evmrelaytypes.ChainContractReader, len(aggregatorContracts)) + for i := range aggregatorContracts { + contractsConfig[fmt.Sprintf("%v_%v", ccip.OFFCHAIN_AGGREGATOR, i)] = evmrelaytypes.ChainContractReader{ + ContractABI: ccip.OffChainAggregatorABI, + Configs: map[string]*evmrelaytypes.ChainReaderDefinition{ + "decimals": { // CR consumers choose an alias + ChainSpecificName: "decimals", + }, + "latestRoundData": { + ChainSpecificName: "latestRoundData", + }, + }, + } + } + contractReaderConfig := evmrelaytypes.ChainReaderConfig{ + Contracts: contractsConfig, + } + + contractReaderConfigJsonBytes, jerr := json.Marshal(contractReaderConfig) + if jerr != nil { + return nil, fmt.Errorf("marshal contract reader config: %w", jerr) + } + + contractReader, cerr := relay.NewContractReader(ctx, contractReaderConfigJsonBytes) + if cerr != nil { + return nil, fmt.Errorf("new ccip commit contract reader %w", cerr) + } + + contractReaders[chainID] = contractReader + } + + priceGetter, err = ccip.NewDynamicPriceGetter(*pluginJobSpecConfig.PriceGetterConfig, contractReaders) + if err != nil { + return nil, fmt.Errorf("creating dynamic price getter: %w", err) + } + } + + return ccipcommit.NewCommitServices(ctx, d.ds, srcProvider, dstProvider, priceGetter, jb, lggr, d.pipelineRunner, oracleArgsNoPlugin, d.isNewlyCreatedJob, int64(srcChainID), dstChainID, logError) } func newCCIPCommitPluginBytes(isSourceProvider bool, sourceStartBlock uint64, destStartBlock uint64) config.CommitPluginConfig { diff --git a/core/services/ocr2/plugins/ccip/ccipcommit/initializers.go b/core/services/ocr2/plugins/ccip/ccipcommit/initializers.go index c5dc8ffb45..b7543df155 100644 --- a/core/services/ocr2/plugins/ccip/ccipcommit/initializers.go +++ b/core/services/ocr2/plugins/ccip/ccipcommit/initializers.go @@ -3,14 +3,9 @@ package ccipcommit import ( "context" "encoding/json" - "fmt" "math/big" - "strings" "time" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/pricegetter" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" - "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" libocr2 "github.com/smartcontractkit/libocr/offchainreporting2plus" @@ -27,7 +22,6 @@ import ( db "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdb" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" - "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip" @@ -48,7 +42,7 @@ var defaultNewReportingPluginRetryConfig = ccipdata.RetryConfig{ MaxRetries: (6 * 4) + 10, } -func NewCommitServices(ctx context.Context, ds sqlutil.DataSource, srcProvider commontypes.CCIPCommitProvider, dstProvider commontypes.CCIPCommitProvider, chainSet legacyevm.LegacyChainContainer, jb job.Job, lggr logger.Logger, pr pipeline.Runner, argsNoPlugin libocr2.OCR2OracleArgs, new bool, sourceChainID int64, destChainID int64, logError func(string)) ([]job.ServiceCtx, error) { +func NewCommitServices(ctx context.Context, ds sqlutil.DataSource, srcProvider commontypes.CCIPCommitProvider, dstProvider commontypes.CCIPCommitProvider, priceGetter ccip.AllTokensPriceGetter, jb job.Job, lggr logger.Logger, pr pipeline.Runner, argsNoPlugin libocr2.OCR2OracleArgs, new bool, sourceChainID int64, destChainID int64, logError func(string)) ([]job.ServiceCtx, error) { spec := jb.OCR2OracleSpec var pluginConfig ccipconfig.CommitPluginJobSpecConfig @@ -75,45 +69,6 @@ func NewCommitServices(ctx context.Context, ds sqlutil.DataSource, srcProvider c commitStoreReader = ccip.NewProviderProxyCommitStoreReader(srcCommitStore, dstCommitStore) commitLggr := lggr.Named("CCIPCommit").With("sourceChain", sourceChainID, "destChain", destChainID) - var priceGetter pricegetter.AllTokensPriceGetter - withPipeline := strings.Trim(pluginConfig.TokenPricesUSDPipeline, "\n\t ") != "" - if withPipeline { - priceGetter, err = pricegetter.NewPipelineGetter(pluginConfig.TokenPricesUSDPipeline, pr, jb.ID, jb.ExternalJobID, jb.Name.ValueOrZero(), lggr) - if err != nil { - return nil, fmt.Errorf("creating pipeline price getter: %w", err) - } - } else { - // Use dynamic price getter. - if pluginConfig.PriceGetterConfig == nil { - return nil, fmt.Errorf("priceGetterConfig is nil") - } - - // Build price getter clients for all chains specified in the aggregator configurations. - // Some lanes (e.g. Wemix/Kroma) requires other clients than source and destination, since they use feeds from other chains. - priceGetterClients := map[uint64]pricegetter.DynamicPriceGetterClient{} - for _, aggCfg := range pluginConfig.PriceGetterConfig.AggregatorPrices { - chainID := aggCfg.ChainID - // Retrieve the chain. - chain, _, err2 := ccipconfig.GetChainByChainID(chainSet, chainID) - if err2 != nil { - return nil, fmt.Errorf("retrieving chain for chainID %d: %w", chainID, err2) - } - caller := rpclib.NewDynamicLimitedBatchCaller( - lggr, - chain.Client(), - rpclib.DefaultRpcBatchSizeLimit, - rpclib.DefaultRpcBatchBackOffMultiplier, - rpclib.DefaultMaxParallelRpcCalls, - ) - priceGetterClients[chainID] = pricegetter.NewDynamicPriceGetterClient(caller) - } - - priceGetter, err = pricegetter.NewDynamicPriceGetter(*pluginConfig.PriceGetterConfig, priceGetterClients) - if err != nil { - return nil, fmt.Errorf("creating dynamic price getter: %w", err) - } - } - offRampReader, err := dstProvider.NewOffRampReader(ctx, pluginConfig.OffRamp) if err != nil { return nil, err diff --git a/core/services/ocr2/plugins/ccip/exportinternal.go b/core/services/ocr2/plugins/ccip/exportinternal.go index 2f924085fb..be39346984 100644 --- a/core/services/ocr2/plugins/ccip/exportinternal.go +++ b/core/services/ocr2/plugins/ccip/exportinternal.go @@ -5,12 +5,16 @@ import ( "math/big" "time" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" "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/internal/gethwrappers2/generated/offchainaggregator" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" @@ -21,8 +25,13 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipdata/v1_2_0" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/pricegetter" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" ) +const OFFCHAIN_AGGREGATOR = "OffchainAggregator" +const DECIMALS_METHOD_NAME = "decimals" +const LATEST_ROUND_DATA_METHOD_NAME = "latestRoundData" + func GenericAddrToEvm(addr ccip.Address) (common.Address, error) { return ccipcalc.GenericAddrToEvm(addr) } @@ -71,12 +80,18 @@ type DynamicPriceGetterClient = pricegetter.DynamicPriceGetterClient type DynamicPriceGetter = pricegetter.DynamicPriceGetter +type AllTokensPriceGetter = pricegetter.AllTokensPriceGetter + +func NewPipelineGetter(source string, runner pipeline.Runner, jobID int32, externalJobID uuid.UUID, name string, lggr logger.Logger) (*pricegetter.PipelineGetter, error) { + return pricegetter.NewPipelineGetter(source, runner, jobID, externalJobID, name, lggr) +} + func NewDynamicPriceGetterClient(batchCaller rpclib.EvmBatchCaller) DynamicPriceGetterClient { return pricegetter.NewDynamicPriceGetterClient(batchCaller) } -func NewDynamicPriceGetter(cfg config.DynamicPriceGetterConfig, evmClients map[uint64]DynamicPriceGetterClient) (*DynamicPriceGetter, error) { - return pricegetter.NewDynamicPriceGetter(cfg, evmClients) +func NewDynamicPriceGetter(cfg config.DynamicPriceGetterConfig, contractReaders map[uint64]types.ContractReader) (*DynamicPriceGetter, error) { + return pricegetter.NewDynamicPriceGetter(cfg, contractReaders) } func NewDynamicLimitedBatchCaller( @@ -133,3 +148,5 @@ func NewCommitOffchainConfig( ) ccip.CommitOffchainConfig { return ccipdata.NewCommitOffchainConfig(gasPriceDeviationPPB, gasPriceHeartBeat, tokenPriceDeviationPPB, tokenPriceHeartBeat, inflightCacheExpiry, priceReportingDisabled) } + +const OffChainAggregatorABI = offchainaggregator.OffchainAggregatorABI diff --git a/core/services/ocr2/plugins/ccip/internal/pricegetter/evm.go b/core/services/ocr2/plugins/ccip/internal/pricegetter/evm.go index ac4002f53f..48ad9f3298 100644 --- a/core/services/ocr2/plugins/ccip/internal/pricegetter/evm.go +++ b/core/services/ocr2/plugins/ccip/internal/pricegetter/evm.go @@ -7,20 +7,22 @@ import ( "math/big" "strings" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "go.uber.org/multierr" + "github.com/smartcontractkit/chainlink-common/pkg/types" cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccip" - + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/aggregator_v3_interface" "github.com/smartcontractkit/chainlink/v2/core/internal/gethwrappers2/generated/offchainaggregator" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/ccipcalc" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/internal/rpclib" ) -const decimalsMethodName = "decimals" -const latestRoundDataMethodName = "latestRoundData" +const OFFCHAIN_AGGREGATOR = "OffchainAggregator" +const DECIMALS_METHOD_NAME = "decimals" +const LATEST_ROUND_DATA_METHOD_NAME = "latestRoundData" func init() { // Ensure existence of latestRoundData method on the Aggregator contract. @@ -28,8 +30,8 @@ func init() { if err != nil { panic(err) } - ensureMethodOnContract(aggregatorABI, decimalsMethodName) - ensureMethodOnContract(aggregatorABI, latestRoundDataMethodName) + ensureMethodOnContract(aggregatorABI, DECIMALS_METHOD_NAME) + ensureMethodOnContract(aggregatorABI, LATEST_ROUND_DATA_METHOD_NAME) } func ensureMethodOnContract(abi abi.ABI, methodName string) { @@ -49,9 +51,9 @@ func NewDynamicPriceGetterClient(batchCaller rpclib.EvmBatchCaller) DynamicPrice } type DynamicPriceGetter struct { - cfg config.DynamicPriceGetterConfig - evmClients map[uint64]DynamicPriceGetterClient - aggregatorAbi abi.ABI + cfg config.DynamicPriceGetterConfig + contractReaders map[uint64]types.ContractReader + aggregatorAbi abi.ABI } func NewDynamicPriceGetterConfig(configJson string) (config.DynamicPriceGetterConfig, error) { @@ -69,7 +71,7 @@ func NewDynamicPriceGetterConfig(configJson string) (config.DynamicPriceGetterCo // NewDynamicPriceGetter build a DynamicPriceGetter from a configuration and a map of chain ID to batch callers. // A batch caller should be provided for all retrieved prices. -func NewDynamicPriceGetter(cfg config.DynamicPriceGetterConfig, evmClients map[uint64]DynamicPriceGetterClient) (*DynamicPriceGetter, error) { +func NewDynamicPriceGetter(cfg config.DynamicPriceGetterConfig, contractReaders map[uint64]types.ContractReader) (*DynamicPriceGetter, error) { if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("validating dynamic price getter config: %w", err) } @@ -77,13 +79,13 @@ func NewDynamicPriceGetter(cfg config.DynamicPriceGetterConfig, evmClients map[u if err != nil { return nil, fmt.Errorf("parsing offchainaggregator abi: %w", err) } - priceGetter := DynamicPriceGetter{cfg, evmClients, aggregatorAbi} + priceGetter := DynamicPriceGetter{cfg, contractReaders, aggregatorAbi} return &priceGetter, nil } // FilterConfiguredTokens implements the PriceGetter interface. // It filters a list of token addresses for only those that have a price resolution rule configured on the PriceGetterConfig -func (d *DynamicPriceGetter) FilterConfiguredTokens(ctx context.Context, tokens []cciptypes.Address) (configured []cciptypes.Address, unconfigured []cciptypes.Address, err error) { +func (d *DynamicPriceGetter) FilterConfiguredTokens(_ context.Context, tokens []cciptypes.Address) (configured []cciptypes.Address, unconfigured []cciptypes.Address, err error) { configured = []cciptypes.Address{} unconfigured = []cciptypes.Address{} for _, tk := range tokens { @@ -103,7 +105,7 @@ func (d *DynamicPriceGetter) FilterConfiguredTokens(ctx context.Context, tokens return configured, unconfigured, nil } -// It returns the prices of all tokens defined in the price getter. +// GetJobSpecTokenPricesUSD returns the prices of all tokens defined in the price getter. func (d *DynamicPriceGetter) GetJobSpecTokenPricesUSD(ctx context.Context) (map[cciptypes.Address]*big.Int, error) { return d.TokenPricesUSD(ctx, d.getAllTokensDefined()) } @@ -144,60 +146,104 @@ func (d *DynamicPriceGetter) performBatchCalls(ctx context.Context, batchCallsPe } // performBatchCall performs a batch call on a given chain to retrieve token prices. -func (d *DynamicPriceGetter) performBatchCall(ctx context.Context, chainID uint64, batchCalls *batchCallsForChain, prices map[cciptypes.Address]*big.Int) error { - // Retrieve the EVM caller for the chain. - client, exists := d.evmClients[chainID] - if !exists { - return fmt.Errorf("evm caller for chain %d not found", chainID) - } - evmCaller := client.BatchCaller - +func (d *DynamicPriceGetter) performBatchCall(ctx context.Context, chainID uint64, batchCalls *batchCallsForChain, prices map[cciptypes.Address]*big.Int) (err error) { nbDecimalCalls := len(batchCalls.decimalCalls) nbLatestRoundDataCalls := len(batchCalls.decimalCalls) + nbCalls := len(batchCalls.decimalCalls) - // Perform batched call (all decimals calls followed by latest round data calls). - calls := make([]rpclib.EvmCall, 0, nbDecimalCalls+nbLatestRoundDataCalls) - calls = append(calls, batchCalls.decimalCalls...) - calls = append(calls, batchCalls.latestRoundDataCalls...) + // Retrieve contract reader for the chain + contractReader := d.contractReaders[chainID] - results, err := evmCaller.BatchCall(ctx, 0, calls) + // Bind contract reader to the contract addresses necessary for the batch calls + bindings := make([]types.BoundContract, 0) + for i, call := range batchCalls.decimalCalls { + bindings = append(bindings, types.BoundContract{ + Address: string(ccipcalc.EvmAddrToGeneric(call.ContractAddress())), + Name: fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i), + }) + } + + err = contractReader.Bind(ctx, bindings) if err != nil { - return fmt.Errorf("batch call on chain %d failed: %w", chainID, err) + return fmt.Errorf("binding contracts failed: %w", err) } - // Extract results. - decimals := make([]uint8, 0, nbDecimalCalls) - latestRounds := make([]*big.Int, 0, nbLatestRoundDataCalls) + // Construct request, adding a decimals and latestRound req per contract name + var decimalsReq uint8 + batchGetLatestValuesRequest := make(map[string]types.ContractBatch) + for i, call := range batchCalls.decimalCalls { + contractName := fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i) + batchGetLatestValuesRequest[contractName] = append(batchGetLatestValuesRequest[contractName], types.BatchRead{ + ReadName: call.MethodName(), + ReturnVal: &decimalsReq, + }) + } - for i, res := range results[0:nbDecimalCalls] { - v, err1 := rpclib.ParseOutput[uint8](res, 0) - if err1 != nil { - callSignature := batchCalls.decimalCalls[i].String() - return fmt.Errorf("parse contract output while calling %v on chain %d: %w", callSignature, chainID, err1) - } - decimals = append(decimals, v) + for i, call := range batchCalls.latestRoundDataCalls { + contractName := fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i) + batchGetLatestValuesRequest[contractName] = append(batchGetLatestValuesRequest[contractName], types.BatchRead{ + ReadName: call.MethodName(), + ReturnVal: &aggregator_v3_interface.LatestRoundData{}, + }) + } + + // Perform call + result, err2 := contractReader.BatchGetLatestValues(ctx, batchGetLatestValuesRequest) + if err2 != nil { + return fmt.Errorf("BatchGetLatestValues failed %w", err2) } - for i, res := range results[nbDecimalCalls : nbDecimalCalls+nbLatestRoundDataCalls] { - // latestRoundData function has multiple outputs (roundId,answer,startedAt,updatedAt,answeredInRound). - // we want the second one (answer, at idx=1). - v, err1 := rpclib.ParseOutput[*big.Int](res, 1) - if err1 != nil { - callSignature := batchCalls.latestRoundDataCalls[i].String() - return fmt.Errorf("parse contract output while calling %v on chain %d: %w", callSignature, chainID, err1) + // Extract results + // give result the contract name (key ordering not guaranteed to match that of the request) + // and then you get slice of responses + decimalsCR := make([]uint8, 0, nbDecimalCalls) + latestRoundCR := make([]aggregator_v3_interface.LatestRoundData, 0, nbDecimalCalls) + var respErr error + for j := range nbCalls { + contractName := fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, j) + offchainAggregatorRespSlice := result[contractName] + + for i, read := range offchainAggregatorRespSlice { + val, readErr := read.GetResult() + if readErr != nil { + respErr = multierr.Append(respErr, fmt.Errorf("error with method call %v: %w", batchCalls.decimalCalls[i].MethodName(), readErr)) + continue + } + if read.ReadName == DECIMALS_METHOD_NAME { + decimal, ok := val.(*uint8) + if !ok { + return fmt.Errorf("expected type uint8 for method call %v on contract %v: %w", batchCalls.decimalCalls[i].MethodName(), batchCalls.decimalCalls[i].ContractAddress(), readErr) + } + + decimalsCR = append(decimalsCR, *decimal) + } else if read.ReadName == LATEST_ROUND_DATA_METHOD_NAME { + latestRoundDataRes, ok := val.(*aggregator_v3_interface.LatestRoundData) + if !ok { + return fmt.Errorf("expected type latestRoundDataConfig for method call %v on contract %v: %w", batchCalls.latestRoundDataCalls[i/2].MethodName(), batchCalls.latestRoundDataCalls[i/2].ContractAddress(), readErr) + } + + latestRoundCR = append(latestRoundCR, *latestRoundDataRes) + } } - latestRounds = append(latestRounds, v) + } + if respErr != nil { + return respErr + } + + latestRoundAnswerCR := make([]*big.Int, 0, nbLatestRoundDataCalls) + for i := range nbLatestRoundDataCalls { + latestRoundAnswerCR = append(latestRoundAnswerCR, latestRoundCR[i].Answer) } // Normalize and store prices. for i := range batchCalls.tokenOrder { // Normalize to 1e18. - if decimals[i] < 18 { - latestRounds[i].Mul(latestRounds[i], big.NewInt(0).Exp(big.NewInt(10), big.NewInt(18-int64(decimals[i])), nil)) - } else if decimals[i] > 18 { - latestRounds[i].Div(latestRounds[i], big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(decimals[i])-18), nil)) + if decimalsCR[i] < 18 { + latestRoundAnswerCR[i].Mul(latestRoundAnswerCR[i], big.NewInt(0).Exp(big.NewInt(10), big.NewInt(18-int64(decimalsCR[i])), nil)) + } else if decimalsCR[i] > 18 { + latestRoundAnswerCR[i].Div(latestRoundAnswerCR[i], big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(decimalsCR[i])-18), nil)) } - prices[ccipcalc.EvmAddrToGeneric(batchCalls.tokenOrder[i])] = latestRounds[i] + prices[ccipcalc.EvmAddrToGeneric(batchCalls.tokenOrder[i])] = latestRoundAnswerCR[i] } return nil } @@ -225,12 +271,12 @@ func (d *DynamicPriceGetter) preparePricesAndBatchCallsPerChain(tokens []cciptyp chainCalls := batchCallsPerChain[aggCfg.ChainID] chainCalls.decimalCalls = append(chainCalls.decimalCalls, rpclib.NewEvmCall( d.aggregatorAbi, - decimalsMethodName, + DECIMALS_METHOD_NAME, aggCfg.AggregatorContractAddress, )) chainCalls.latestRoundDataCalls = append(chainCalls.latestRoundDataCalls, rpclib.NewEvmCall( d.aggregatorAbi, - latestRoundDataMethodName, + LATEST_ROUND_DATA_METHOD_NAME, aggCfg.AggregatorContractAddress, )) chainCalls.tokenOrder = append(chainCalls.tokenOrder, tk) diff --git a/core/services/ocr2/plugins/ccip/internal/pricegetter/evm_test.go b/core/services/ocr2/plugins/ccip/internal/pricegetter/evm_test.go index 78de269968..755bcb84eb 100644 --- a/core/services/ocr2/plugins/ccip/internal/pricegetter/evm_test.go +++ b/core/services/ocr2/plugins/ccip/internal/pricegetter/evm_test.go @@ -1,9 +1,14 @@ package pricegetter import ( + "fmt" "math/big" "testing" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets/mocks" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -23,6 +28,7 @@ import ( type testParameters struct { cfg config.DynamicPriceGetterConfig evmClients map[uint64]DynamicPriceGetterClient + contractReaders map[uint64]types.ContractReader tokens []common.Address expectedTokenPrices map[common.Address]big.Int expectedTokenPricesForAll map[common.Address]big.Int @@ -92,13 +98,13 @@ func TestDynamicPriceGetterWithEmptyInput(t *testing.T) { }, { name: "get_all_tokens_static_only", - param: testGetAllTokensStaticOnly(), + param: testGetAllTokensStaticOnly(t), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - pg, err := NewDynamicPriceGetter(test.param.cfg, test.param.evmClients) + pg, err := NewDynamicPriceGetter(test.param.cfg, test.param.contractReaders) if test.param.invalidConfigErrorExpected { require.Error(t, err) return @@ -200,6 +206,12 @@ func testParamAggregatorOnly(t *testing.T) testParameters { uint64(103): mockClient(t, []uint8{18}, []aggregator_v3_interface.LatestRoundData{round3}), uint64(104): mockClient(t, []uint8{20}, []aggregator_v3_interface.LatestRoundData{round4}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + uint64(103): mockCR(t, []uint8{18}, []aggregator_v3_interface.LatestRoundData{round3}), + uint64(104): mockCR(t, []uint8{20}, []aggregator_v3_interface.LatestRoundData{round4}), + } expectedTokenPrices := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), // expected in 1e18 format. TK2: *multExp(round2.Answer, 10), // expected in 1e18 format. @@ -209,6 +221,7 @@ func testParamAggregatorOnly(t *testing.T) testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3, TK4}, expectedTokenPrices: expectedTokenPrices, invalidConfigErrorExpected: false, @@ -261,6 +274,10 @@ func testParamAggregatorOnlyMulti(t *testing.T) testParameters { uint64(101): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), uint64(102): mockClient(t, []uint8{8, 8}, []aggregator_v3_interface.LatestRoundData{round2, round3}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8, 8}, []aggregator_v3_interface.LatestRoundData{round2, round3}), + } expectedTokenPrices := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), TK2: *multExp(round2.Answer, 10), @@ -269,6 +286,7 @@ func testParamAggregatorOnlyMulti(t *testing.T) testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, invalidConfigErrorExpected: false, tokens: []common.Address{TK1, TK2, TK3}, expectedTokenPrices: expectedTokenPrices, @@ -295,6 +313,7 @@ func testParamStaticOnly() testParameters { } // Real LINK/USD example from OP. evmClients := map[uint64]DynamicPriceGetterClient{} + contractReaders := map[uint64]types.ContractReader{} expectedTokenPrices := map[common.Address]big.Int{ TK1: *cfg.StaticPrices[TK1].Price, TK2: *cfg.StaticPrices[TK2].Price, @@ -303,6 +322,7 @@ func testParamStaticOnly() testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3}, expectedTokenPrices: expectedTokenPrices, } @@ -347,6 +367,10 @@ func testParamNoAggregatorForToken(t *testing.T) testParameters { uint64(101): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + } expectedTokenPrices := map[common.Address]big.Int{ TK1: *round1.Answer, TK2: *round2.Answer, @@ -356,6 +380,7 @@ func testParamNoAggregatorForToken(t *testing.T) testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3, TK4}, expectedTokenPrices: expectedTokenPrices, priceResolutionErrorExpected: true, @@ -401,6 +426,10 @@ func testParamAggregatorAndStaticValid(t *testing.T) testParameters { uint64(101): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + } expectedTokenPrices := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), TK2: *multExp(round2.Answer, 10), @@ -409,6 +438,7 @@ func testParamAggregatorAndStaticValid(t *testing.T) testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3}, expectedTokenPrices: expectedTokenPrices, } @@ -465,9 +495,15 @@ func testParamAggregatorAndStaticTokenCollision(t *testing.T) testParameters { uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), uint64(103): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + uint64(103): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), + } return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3}, invalidConfigErrorExpected: true, } @@ -506,11 +542,16 @@ func testParamBatchCallReturnsErr(t *testing.T) testParameters { BatchCaller: mockErrCaller(t), }, } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockErrCR(t), + } return testParameters{ - cfg: cfg, - evmClients: evmClients, - tokens: []common.Address{TK1, TK2, TK3}, - evmCallErr: true, + cfg: cfg, + evmClients: evmClients, + contractReaders: contractReaders, + tokens: []common.Address{TK1, TK2, TK3}, + evmCallErr: true, } } @@ -566,6 +607,11 @@ func testLessInputsThanDefinedPrices(t *testing.T) testParameters { uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), uint64(103): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + uint64(103): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), + } expectedTokenPrices := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), TK2: *multExp(round2.Answer, 10), @@ -574,6 +620,7 @@ func testLessInputsThanDefinedPrices(t *testing.T) testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, tokens: []common.Address{TK1, TK2, TK3}, expectedTokenPrices: expectedTokenPrices, } @@ -631,6 +678,11 @@ func testGetAllTokensAggregatorAndStatic(t *testing.T) testParameters { uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), uint64(103): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + uint64(103): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), + } expectedTokenPricesForAll := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), TK2: *multExp(round2.Answer, 10), @@ -641,6 +693,7 @@ func testGetAllTokensAggregatorAndStatic(t *testing.T) testParameters { cfg: cfg, evmClients: evmClients, expectedTokenPricesForAll: expectedTokenPricesForAll, + contractReaders: contractReaders, } } @@ -691,6 +744,12 @@ func testGetAllTokensAggregatorOnly(t *testing.T) testParameters { uint64(102): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), uint64(103): mockClient(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), } + contractReaders := map[uint64]types.ContractReader{ + uint64(101): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round1}), + uint64(102): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round2}), + uint64(103): mockCR(t, []uint8{8}, []aggregator_v3_interface.LatestRoundData{round3}), + } + expectedTokenPricesForAll := map[common.Address]big.Int{ TK1: *multExp(round1.Answer, 10), TK2: *multExp(round2.Answer, 10), @@ -700,10 +759,11 @@ func testGetAllTokensAggregatorOnly(t *testing.T) testParameters { cfg: cfg, evmClients: evmClients, expectedTokenPricesForAll: expectedTokenPricesForAll, + contractReaders: contractReaders, } } -func testGetAllTokensStaticOnly() testParameters { +func testGetAllTokensStaticOnly(t *testing.T) testParameters { cfg := config.DynamicPriceGetterConfig{ AggregatorPrices: map[common.Address]config.AggregatorPriceConfig{}, StaticPrices: map[common.Address]config.StaticPriceConfig{ @@ -723,6 +783,7 @@ func testGetAllTokensStaticOnly() testParameters { } evmClients := map[uint64]DynamicPriceGetterClient{} + contractReaders := map[uint64]types.ContractReader{} expectedTokenPricesForAll := map[common.Address]big.Int{ TK1: *cfg.StaticPrices[TK1].Price, TK2: *cfg.StaticPrices[TK2].Price, @@ -731,6 +792,7 @@ func testGetAllTokensStaticOnly() testParameters { return testParameters{ cfg: cfg, evmClients: evmClients, + contractReaders: contractReaders, expectedTokenPricesForAll: expectedTokenPricesForAll, } } @@ -760,12 +822,54 @@ func mockCaller(t *testing.T, decimals []uint8, rounds []aggregator_v3_interface return caller } +func mockCR(t *testing.T, decimals []uint8, rounds []aggregator_v3_interface.LatestRoundData) *mocks.ChainReader { + caller := mocks.NewChainReader(t) + + // Mock batch calls per chain: all decimals calls then all latestRoundData calls. + // bGLVR = batchGetLatestValueResult + //nolint:all + var bGLVR types.BatchGetLatestValuesResult + bGLVR = make(map[string]types.ContractBatchResults, 1) + + for i := range len(decimals) { + bGLVR[fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i)] = make([]types.BatchReadResult, 0, 2) + } + for i, d := range decimals { + contractName := fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i) + readRes := types.BatchReadResult{ + ReadName: DECIMALS_METHOD_NAME, + } + readRes.SetResult(&d, nil) + bGLVR[contractName] = append(bGLVR[contractName], readRes) + } + + for i, r := range rounds { + contractName := fmt.Sprintf("%v_%v", OFFCHAIN_AGGREGATOR, i) + readRes := types.BatchReadResult{ + ReadName: LATEST_ROUND_DATA_METHOD_NAME, + } + readRes.SetResult(&r, nil) + bGLVR[contractName] = append(bGLVR[contractName], readRes) + } + + caller.On("Bind", mock.Anything, mock.Anything).Return(nil).Maybe() + caller.On("BatchGetLatestValues", mock.Anything, mock.Anything).Return(bGLVR, nil).Maybe() + return caller +} + func mockErrCaller(t *testing.T) *rpclibmocks.EvmBatchCaller { caller := rpclibmocks.NewEvmBatchCaller(t) caller.On("BatchCall", mock.Anything, uint64(0), mock.Anything).Return(nil, assert.AnError).Maybe() return caller } +func mockErrCR(t *testing.T) *mocks.ChainReader { + caller := mocks.NewChainReader(t) + caller.On("Bind", mock.Anything, mock.Anything).Return(nil).Maybe() + caller.On("BatchGetLatestValues", mock.Anything, mock.Anything).Return(nil, assert.AnError).Maybe() + return caller +} + // multExp returns the result of multiplying x by 10^e. func multExp(x *big.Int, e int64) *big.Int { return big.NewInt(0).Mul(x, big.NewInt(0).Exp(big.NewInt(10), big.NewInt(e), nil)) diff --git a/core/services/ocr2/plugins/ccip/internal/rpclib/evm.go b/core/services/ocr2/plugins/ccip/internal/rpclib/evm.go index 71357029dd..876d9fa782 100644 --- a/core/services/ocr2/plugins/ccip/internal/rpclib/evm.go +++ b/core/services/ocr2/plugins/ccip/internal/rpclib/evm.go @@ -280,6 +280,10 @@ func (c EvmCall) String() string { return fmt.Sprintf("%s: %s(%+v)", c.contractAddress.String(), c.methodName, c.args) } +func (c EvmCall) ContractAddress() common.Address { + return c.contractAddress +} + func EVMCallsToString(calls []EvmCall) string { callString := "" for _, call := range calls { diff --git a/integration-tests/ccip-tests/Makefile b/integration-tests/ccip-tests/Makefile index 8fdd635fd6..40b4cc8338 100644 --- a/integration-tests/ccip-tests/Makefile +++ b/integration-tests/ccip-tests/Makefile @@ -67,3 +67,10 @@ test_smoke_ccip_default: set_config .PHONY: build_ccip_image build_ccip_image: docker build -f ../../core/chainlink.Dockerfile --build-arg COMMIT_SHA=$(git rev-parse HEAD) --build-arg CHAINLINK_USER=chainlink -t $(image):$(tag) ../../ + +# image: the name for the chainlink image being built, example: image=chainlink +# tag: the tag for the chainlink image being built, example: tag=latest +# example usage: make build_ccip_image image=chainlink-ccip tag=latest +.PHONY: build_ccip_debug_image +build_ccip_debug_image: + docker build -f ../../core/chainlink.debug.Dockerfile --build-arg COMMIT_SHA=$(git rev-parse HEAD) --build-arg CHAINLINK_USER=chainlink -t $(image):$(tag) ../../ diff --git a/integration-tests/ccip-tests/actions/ccip_helpers.go b/integration-tests/ccip-tests/actions/ccip_helpers.go index 2bbc77f1c2..eb34db40de 100644 --- a/integration-tests/ccip-tests/actions/ccip_helpers.go +++ b/integration-tests/ccip-tests/actions/ccip_helpers.go @@ -1648,7 +1648,10 @@ func (sourceCCIP *SourceCCIPModule) IsRequestTriggeredWithinTimeframe(timeframe // IsPastRequestTriggeredWithinTimeframe determines the average block time and calculates the block numbers // within the specified timeframe. It then uses FilterCCIPSendRequested to identify the past events. -func (sourceCCIP *SourceCCIPModule) IsPastRequestTriggeredWithinTimeframe(ctx context.Context, timeframe *commonconfig.Duration) (*time.Time, error) { +func (sourceCCIP *SourceCCIPModule) IsPastRequestTriggeredWithinTimeframe( + ctx context.Context, + timeframe *commonconfig.Duration, +) (*types.Log, error) { if timeframe == nil { return nil, nil } @@ -1675,17 +1678,21 @@ func (sourceCCIP *SourceCCIPModule) IsPastRequestTriggeredWithinTimeframe(ctx co return nil, fmt.Errorf("error while filtering CCIP send requested starting block number: %d. Error: %w", filterFromBlock, err) } defer func() { - _ = iterator.Close() + iterErr := iterator.Close() + if iterErr != nil { + sourceCCIP.Common.Logger.Error().Err(iterErr).Msg("Error closing iterator") + } }() - if iterator.Next() { - hdr, err := sourceCCIP.Common.ChainClient.HeaderByNumber(context.Background(), big.NewInt(int64(iterator.Event.Raw.BlockNumber))) - if err != nil { - return nil, fmt.Errorf("error getting header for block: %d, Error: %w", iterator.Event.Raw.BlockNumber, err) + lastBlockNumber := uint64(0) + var latestEvent *types.Log + for iterator.Next() { + blockNum := iterator.Event.Raw.BlockNumber + if blockNum > lastBlockNumber { + lastBlockNumber = blockNum + latestEvent = &iterator.Event.Raw } - return pointer.ToTime(hdr.Timestamp), nil } - - return nil, nil + return latestEvent, nil } func (sourceCCIP *SourceCCIPModule) AssertEventCCIPSendRequested( diff --git a/integration-tests/ccip-tests/load/ccip_loadgen.go b/integration-tests/ccip-tests/load/ccip_loadgen.go index d3af128309..89804dbf8a 100644 --- a/integration-tests/ccip-tests/load/ccip_loadgen.go +++ b/integration-tests/ccip-tests/load/ccip_loadgen.go @@ -226,13 +226,19 @@ func (c *CCIPE2ELoad) Call(_ *wasp.Generator) *wasp.Response { res := &wasp.Response{} sourceCCIP := c.Lane.Source var recentRequestFoundAt *time.Time + var latestEvent *types.Log var err error // Use IsPastRequestTriggeredWithinTimeframe to check for any historical CCIP send request events // within the specified timeframe for the first message. Subsequently, use the watcher method to monitor // and detect any new events as they occur. if c.CurrentMsgSerialNo.Load() == int64(1) { - recentRequestFoundAt, err = sourceCCIP.IsPastRequestTriggeredWithinTimeframe(testcontext.Get(c.t), c.SkipRequestIfAnotherRequestTriggeredWithin) + latestEvent, err = sourceCCIP.IsPastRequestTriggeredWithinTimeframe(testcontext.Get(c.t), c.SkipRequestIfAnotherRequestTriggeredWithin) require.NoError(c.t, err, "error while filtering past requests") + if latestEvent != nil { + hdr, err := sourceCCIP.Common.ChainClient.HeaderByNumber(context.Background(), big.NewInt(int64(latestEvent.BlockNumber))) + require.NoError(c.t, err, "error while getting header by block number") + recentRequestFoundAt = pointer.ToTime(hdr.Timestamp) + } } else { recentRequestFoundAt = sourceCCIP.IsRequestTriggeredWithinTimeframe(c.SkipRequestIfAnotherRequestTriggeredWithin) } diff --git a/integration-tests/ccip-tests/load/ccip_multicall_loadgen.go b/integration-tests/ccip-tests/load/ccip_multicall_loadgen.go index 04fcffaa4b..39e5388244 100644 --- a/integration-tests/ccip-tests/load/ccip_multicall_loadgen.go +++ b/integration-tests/ccip-tests/load/ccip_multicall_loadgen.go @@ -91,7 +91,7 @@ func NewMultiCallLoadGenerator(testCfg *testsetups.CCIPTestConfig, lanes []*acti testCfg.Test, lane, testCfg.TestGroupInput.PhaseTimeout.Duration(), 100000, testCfg.TestGroupInput.LoadProfile.MsgProfile, 0, - testCfg.TestGroupInput.LoadProfile.SkipRequestIfAnotherRequestTriggeredWithin, + testCfg.TestGroupInput.SkipRequestIfAnotherRequestTriggeredWithin, ) ccipLoad.BeforeAllCall() m.E2ELoads[fmt.Sprintf("%s-%s", lane.SourceNetworkName, lane.DestNetworkName)] = ccipLoad diff --git a/integration-tests/ccip-tests/load/helper.go b/integration-tests/ccip-tests/load/helper.go index 3465b58204..2c150bbc5a 100644 --- a/integration-tests/ccip-tests/load/helper.go +++ b/integration-tests/ccip-tests/load/helper.go @@ -276,7 +276,7 @@ func (l *LoadArgs) TriggerLoadByLane() { ccipLoad := NewCCIPLoad( l.TestCfg.Test, lane, l.TestCfg.TestGroupInput.PhaseTimeout.Duration(), 100000, l.TestCfg.TestGroupInput.LoadProfile.MsgProfile, sendMaxData, - l.TestCfg.TestGroupInput.LoadProfile.SkipRequestIfAnotherRequestTriggeredWithin, + l.TestCfg.TestGroupInput.SkipRequestIfAnotherRequestTriggeredWithin, ) ccipLoad.BeforeAllCall() // if it's not multicall set the tokens to nil to free up some space, diff --git a/integration-tests/ccip-tests/smoke/ccip_test.go b/integration-tests/ccip-tests/smoke/ccip_test.go index 83bed78507..bf2533f02a 100644 --- a/integration-tests/ccip-tests/smoke/ccip_test.go +++ b/integration-tests/ccip-tests/smoke/ccip_test.go @@ -3,26 +3,30 @@ package smoke import ( "fmt" "math/big" + "strings" "testing" "time" "github.com/AlekSi/pointer" + "github.com/ethereum/go-ethereum/core/types" "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-testing-framework/lib/logging" + "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/osutil" "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/ptr" + "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/actions" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/lock_release_token_pool" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_pool" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/actions" "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/contracts" "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/testconfig" "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/testreporters" "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/testsetups" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/lock_release_token_pool" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/token_pool" ) type testDefinition struct { @@ -906,6 +910,123 @@ func TestSmokeCCIPReorgAboveFinalityAtSource(t *testing.T) { }) } +// TestSmokeCCIPForGivenNetworkPairs is designed specifically for scheduled mainnet testing. This test checks for recent +// transaction and skip the lanes accordingly. This test also has capability to take override input on network pairs and phase timeout. +func TestSmokeCCIPForGivenNetworkPairs(t *testing.T) { + t.Parallel() + log := logging.GetTestLogger(t) + TestCfg := testsetups.NewCCIPTestConfig(t, log, testconfig.Smoke) + // override network pairs + var temp []testsetups.NetworkPair + overrideNetworkPairs, err := osutil.GetEnv("OVERRIDE_NETWORK_PAIRS") + require.NoError(t, err, "Error getting OVERRIDE_NETWORK_PAIRS environment variable") + if overrideNetworkPairs != "" { + networkPairs := strings.Split(overrideNetworkPairs, ";") + for _, networkPair := range networkPairs { + // check for any malformed inputs + if !strings.Contains(networkPair, ",") || len(strings.Split(networkPair, ",")) != 2 { + log.Error().Msgf("malformed OVERRIDE_NETWORK_PAIRS environment variable for network pair: %s ", networkPair) + return + } + networkPair = strings.ToUpper(strings.ReplaceAll(networkPair, "_", " ")) + for _, network := range TestCfg.NetworkPairs { + if strings.Contains(networkPair, strings.ToUpper(network.NetworkA.Name)) && strings.Contains(networkPair, strings.ToUpper(network.NetworkB.Name)) { + temp = append(temp, network) + break + } + } + } + log.Info().Int("Pairs", len(temp)).Msg("Number of lanes overridden in the test") + log.Info().Interface("Lanes", networkPairs).Msg("Lanes under test") + TestCfg.NetworkPairs = temp + } + + // phase timeout override + phaseTimeout, err := osutil.GetEnv("OVERRIDE_PHASE_TIMEOUT") + require.NoError(t, err, "Error getting OVERRIDE_PHASE_TIMEOUT environment variable") + if phaseTimeout != "" { + configDuration, err := config.ParseDuration(phaseTimeout) + require.NoError(t, err, "Error parsing phase timeout value") + TestCfg.TestGroupInput.PhaseTimeout = &configDuration + log.Info().Float64("Timeout in minutes", configDuration.Duration().Minutes()).Msg("Phase timeout is overridden") + } + + gasLimit := big.NewInt(*TestCfg.TestGroupInput.MsgDetails.DestGasLimit) + setUpOutput := testsetups.CCIPDefaultTestSetUp(t, &log, "smoke-ccip", nil, TestCfg) + if len(setUpOutput.Lanes) == 0 { + log.Error().Msg("No lanes found") + return + } + + t.Cleanup(func() { + // If we are running a test that is a token transfer, we need to verify the balance. + // skip the balance check for existing deployment, there can be multiple external requests in progress for existing deployments + // other than token transfer initiated by the test, which can affect the balance check + // therefore we check the balance only for the ccip environment created by the test + if TestCfg.TestGroupInput.MsgDetails.IsTokenTransfer() && + !pointer.GetBool(TestCfg.TestGroupInput.USDCMockDeployment) && + !pointer.GetBool(TestCfg.TestGroupInput.ExistingDeployment) { + setUpOutput.Balance.Verify(t) + } + require.NoError(t, setUpOutput.TearDown(), "error in tear down step") + }) + + var tests []testDefinition + lookBackDuration := TestCfg.TestGroupInput.SkipRequestIfAnotherRequestTriggeredWithin + var recentTxFound *types.Log + + addLanesToTest := func(lane *actions.CCIPLane) { + // Create test definitions for given lane if no previous request has been triggered within the specified timeframe. + // By default, the timeframe is set to nil. To define a timeframe, assign a duration to the variable + // SkipRequestIfAnotherRequestTriggeredWithin. + if lookBackDuration != nil { + recentTxFound, err = lane.Source.IsPastRequestTriggeredWithinTimeframe(lane.Context, lookBackDuration) + require.NoError(t, err, "error while finding recent request for lane network %s to network %s", + lane.SourceNetworkName, lane.DestNetworkName) + } + if recentTxFound == nil { + tests = append(tests, testDefinition{ + testName: fmt.Sprintf("CCIP message transfer from network %s to network %s", + lane.SourceNetworkName, lane.DestNetworkName), + lane: lane, + }) + } else { + log.Info(). + Str("TX", recentTxFound.TxHash.Hex()). + Uint64("Block Number", recentTxFound.BlockNumber). + Str("Source", lane.SourceNetworkName). + Str("Dest", lane.DestNetworkName). + Msgf("Lane Skipped. Recent request found within %v minutes.", lookBackDuration.Duration().Minutes()) + } + } + for _, lane := range setUpOutput.Lanes { + addLanesToTest(lane.ForwardLane) + if lane.ReverseLane != nil { + recentTxFound = nil + addLanesToTest(lane.ReverseLane) + } + } + + // Execute tests. + log.Info().Int("Total Lanes", len(tests)).Msg("Starting CCIP test") + for _, test := range tests { + tc := test + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + tc.lane.Test = t + log.Info(). + Str("Source", tc.lane.SourceNetworkName). + Str("Destination", tc.lane.DestNetworkName). + Msgf("Starting lane %s -> %s", tc.lane.SourceNetworkName, tc.lane.DestNetworkName) + + tc.lane.RecordStateBeforeTransfer() + err = tc.lane.SendRequests(1, gasLimit) + require.NoError(t, err, "error sending requests") + tc.lane.ValidateRequests() + }) + } +} + // performAboveFinalityReorgAndValidate is to perform the above finality reorg test func performAboveFinalityReorgAndValidate(t *testing.T, network string) { t.Helper() diff --git a/integration-tests/ccip-tests/testconfig/README.md b/integration-tests/ccip-tests/testconfig/README.md index 474ac2a3af..cf1431baa9 100644 --- a/integration-tests/ccip-tests/testconfig/README.md +++ b/integration-tests/ccip-tests/testconfig/README.md @@ -657,6 +657,13 @@ Specifies the OCR parameters for the execute job. This is only valid if the test Specifies the value for the `InflightExpiry` in commit job's offchain config. This is only valid if the test is not run on [existing deployments](#ccipgroupstestgroupexistingdeployment). +### CCIP.Groups.[testgroup].SkipRequestIfAnotherRequestTriggeredWithin + +If there is CCIP Send requested event present within this duration, the test will skip sending another +request during load run or avoid sending request in smoke test in that lane. For Example, +if `SkipRequestIfAnotherRequestTriggeredWithin` is set to `40m`, and a request is triggered at 0th second, the test will skip sending another request for another 40m. +This particular field is used to avoid sending transaction when there is traffic already in that lane. + ### CCIP.Groups.[testgroup].OffRampConfig Specifies the offramp configuration for the execution job. This is only valid if the test is not run on [existing deployments](#ccipgroupstestgroupexistingdeployment). @@ -736,11 +743,6 @@ Specifies the duration network delay used for `NetworkChaos` experiment. This is If there are multiple chaos experiments, this specifies the duration to wait between each chaos experiment. This is only valid if the test is run on k8s and not on [existing deployments](#ccipgroupstestgroupexistingdeployment). -#### CCIP.Groups.[testgroup].LoadProfile.SkipRequestIfAnotherRequestTriggeredWithin - -If a request is triggered within this duration, the test will skip sending another request during load run. For Example, if `SkipRequestIfAnotherRequestTriggeredWithin` is set to `40m`, and a request is triggered at 0th second, the test will skip sending another request for another 40m. -This particular field is used to avoid sending multiple requests in a short duration during load run. - #### CCIP.Groups.[testgroup].LoadProfile.OptimizeSpace This is used internally to optimize memory usage during load run. If set to true, after the initial lane set up is over the test will discard the lane config to save memory. diff --git a/integration-tests/ccip-tests/testconfig/ccip.go b/integration-tests/ccip-tests/testconfig/ccip.go index 1f4dfaac51..7925ffa71f 100644 --- a/integration-tests/ccip-tests/testconfig/ccip.go +++ b/integration-tests/ccip-tests/testconfig/ccip.go @@ -211,19 +211,18 @@ type LoadFrequency struct { } type LoadProfile struct { - MsgProfile *MsgProfile `toml:",omitempty"` - FrequencyByDestination map[string]*LoadFrequency `toml:",omitempty"` - RequestPerUnitTime []int64 `toml:",omitempty"` - TimeUnit *config.Duration `toml:",omitempty"` - StepDuration []*config.Duration `toml:",omitempty"` - TestDuration *config.Duration `toml:",omitempty"` - NetworkChaosDelay *config.Duration `toml:",omitempty"` - WaitBetweenChaosDuringLoad *config.Duration `toml:",omitempty"` - SkipRequestIfAnotherRequestTriggeredWithin *config.Duration `toml:",omitempty"` - OptimizeSpace *bool `toml:",omitempty"` - FailOnFirstErrorInLoad *bool `toml:",omitempty"` - SendMaxDataInEveryMsgCount *int64 `toml:",omitempty"` - TestRunName string `toml:",omitempty"` + MsgProfile *MsgProfile `toml:",omitempty"` + FrequencyByDestination map[string]*LoadFrequency `toml:",omitempty"` + RequestPerUnitTime []int64 `toml:",omitempty"` + TimeUnit *config.Duration `toml:",omitempty"` + StepDuration []*config.Duration `toml:",omitempty"` + TestDuration *config.Duration `toml:",omitempty"` + NetworkChaosDelay *config.Duration `toml:",omitempty"` + WaitBetweenChaosDuringLoad *config.Duration `toml:",omitempty"` + OptimizeSpace *bool `toml:",omitempty"` + FailOnFirstErrorInLoad *bool `toml:",omitempty"` + SendMaxDataInEveryMsgCount *int64 `toml:",omitempty"` + TestRunName string `toml:",omitempty"` } func (l *LoadProfile) Validate() error { @@ -242,9 +241,7 @@ func (l *LoadProfile) Validate() error { if l.TestDuration == nil || l.TestDuration.Duration().Minutes() == 0 { return fmt.Errorf("test duration should be set") } - if l.SkipRequestIfAnotherRequestTriggeredWithin != nil && l.TimeUnit.Duration() < l.SkipRequestIfAnotherRequestTriggeredWithin.Duration() { - return fmt.Errorf("SkipRequestIfAnotherRequestTriggeredWithin should be set below the TimeUnit duration") - } + return nil } @@ -265,35 +262,36 @@ func (gp *ReorgProfile) Validate() error { // CCIPTestGroupConfig defines configuration input to change how a particular CCIP test group should run type CCIPTestGroupConfig struct { - Type string `toml:",omitempty"` - KeepEnvAlive *bool `toml:",omitempty"` - BiDirectionalLane *bool `toml:",omitempty"` - CommitAndExecuteOnSameDON *bool `toml:",omitempty"` - AllowOutOfOrder *bool `toml:",omitempty"` // To set out of order execution globally - NoOfCommitNodes int `toml:",omitempty"` - MsgDetails *MsgDetails `toml:",omitempty"` - TokenConfig *TokenConfig `toml:",omitempty"` - MulticallInOneTx *bool `toml:",omitempty"` - NoOfSendsInMulticall int `toml:",omitempty"` - PhaseTimeout *config.Duration `toml:",omitempty"` - LocalCluster *bool `toml:",omitempty"` - ExistingDeployment *bool `toml:",omitempty"` - ReuseContracts *bool `toml:",omitempty"` - NodeFunding float64 `toml:",omitempty"` - NetworkPairs []string `toml:",omitempty"` - DenselyConnectedNetworkChainIds []string `toml:",omitempty"` - NoOfNetworks int `toml:",omitempty"` - NoOfRoutersPerPair int `toml:",omitempty"` - MaxNoOfLanes int `toml:",omitempty"` - ChaosDuration *config.Duration `toml:",omitempty"` - USDCMockDeployment *bool `toml:",omitempty"` - CommitOCRParams *contracts.OffChainAggregatorV2Config `toml:",omitempty"` - ExecOCRParams *contracts.OffChainAggregatorV2Config `toml:",omitempty"` - OffRampConfig *OffRampConfig `toml:",omitempty"` - CommitInflightExpiry *config.Duration `toml:",omitempty"` - StoreLaneConfig *bool `toml:",omitempty"` - LoadProfile *LoadProfile `toml:",omitempty"` - ReorgProfile *ReorgProfile `toml:",omitempty"` + Type string `toml:",omitempty"` + KeepEnvAlive *bool `toml:",omitempty"` + BiDirectionalLane *bool `toml:",omitempty"` + CommitAndExecuteOnSameDON *bool `toml:",omitempty"` + AllowOutOfOrder *bool `toml:",omitempty"` // To set out of order execution globally + NoOfCommitNodes int `toml:",omitempty"` + MsgDetails *MsgDetails `toml:",omitempty"` + TokenConfig *TokenConfig `toml:",omitempty"` + MulticallInOneTx *bool `toml:",omitempty"` + NoOfSendsInMulticall int `toml:",omitempty"` + PhaseTimeout *config.Duration `toml:",omitempty"` + LocalCluster *bool `toml:",omitempty"` + ExistingDeployment *bool `toml:",omitempty"` + ReuseContracts *bool `toml:",omitempty"` + NodeFunding float64 `toml:",omitempty"` + NetworkPairs []string `toml:",omitempty"` + DenselyConnectedNetworkChainIds []string `toml:",omitempty"` + NoOfNetworks int `toml:",omitempty"` + NoOfRoutersPerPair int `toml:",omitempty"` + MaxNoOfLanes int `toml:",omitempty"` + ChaosDuration *config.Duration `toml:",omitempty"` + USDCMockDeployment *bool `toml:",omitempty"` + CommitOCRParams *contracts.OffChainAggregatorV2Config `toml:",omitempty"` + ExecOCRParams *contracts.OffChainAggregatorV2Config `toml:",omitempty"` + OffRampConfig *OffRampConfig `toml:",omitempty"` + CommitInflightExpiry *config.Duration `toml:",omitempty"` + StoreLaneConfig *bool `toml:",omitempty"` + LoadProfile *LoadProfile `toml:",omitempty"` + ReorgProfile *ReorgProfile `toml:",omitempty"` + SkipRequestIfAnotherRequestTriggeredWithin *config.Duration `toml:",omitempty"` } func (c *CCIPTestGroupConfig) Validate() error { @@ -341,6 +339,10 @@ func (c *CCIPTestGroupConfig) Validate() error { return fmt.Errorf("number of sends in multisend should be greater than 0 if multisend is true") } } + if c.SkipRequestIfAnotherRequestTriggeredWithin != nil && c.LoadProfile != nil && + c.LoadProfile.TimeUnit.Duration() < c.SkipRequestIfAnotherRequestTriggeredWithin.Duration() { + return fmt.Errorf("SkipRequestIfAnotherRequestTriggeredWithin should be set below the load TimeUnit duration") + } return nil } diff --git a/integration-tests/ccip-tests/testconfig/override/mainnet.toml b/integration-tests/ccip-tests/testconfig/override/mainnet.toml index f723411eaf..6fa0c3e0dd 100644 --- a/integration-tests/ccip-tests/testconfig/override/mainnet.toml +++ b/integration-tests/ccip-tests/testconfig/override/mainnet.toml @@ -1081,52 +1081,80 @@ TTL = '8h' [CCIP.Env.Network] selected_networks = [ - 'ARBITRUM_MAINNET', - 'AVALANCHE_MAINNET', - 'BSC_MAINNET', - 'BASE_MAINNET', - 'ETHEREUM_MAINNET', - 'KROMA_MAINNET', - 'OPTIMISM_MAINNET', - 'POLYGON_MAINNET', - 'WEMIX_MAINNET', - ] + 'ARBITRUM_MAINNET', + 'AVALANCHE_MAINNET', + 'BASE_MAINNET', + 'BLAST_MAINNET', + 'BSC_MAINNET', + 'CELO_MAINNET', + 'ETHEREUM_MAINNET', + 'GNOSIS_MAINNET', + 'KROMA_MAINNET', + 'METIS_ANDROMEDA', + 'MODE_MAINNET', + 'OPTIMISM_MAINNET', + 'POLYGON_MAINNET', + 'WEMIX_MAINNET', + 'ZKSYNC_MAINNET', +] [CCIP.Groups.load] NetworkPairs = [ - 'ETHEREUM_MAINNET,OPTIMISM_MAINNET', - 'ETHEREUM_MAINNET,AVALANCHE_MAINNET', - 'ETHEREUM_MAINNET,POLYGON_MAINNET', - 'ETHEREUM_MAINNET,BSC_MAINNET', - 'ETHEREUM_MAINNET,ARBITRUM_MAINNET', - 'ETHEREUM_MAINNET,BASE_MAINNET', - 'ETHEREUM_MAINNET,WEMIX_MAINNET', - 'AVALANCHE_MAINNET,POLYGON_MAINNET', - 'BASE_MAINNET,OPTIMISM_MAINNET', - 'BASE_MAINNET,ARBITRUM_MAINNET', - 'AVALANCHE_MAINNET,BSC_MAINNET', - 'BSC_MAINNET,POLYGON_MAINNET', - 'OPTIMISM_MAINNET,POLYGON_MAINNET', - 'BASE_MAINNET,BSC_MAINNET', - 'POLYGON_MAINNET,ARBITRUM_MAINNET', # added as batch 1 - 'ARBITRUM_MAINNET,BSC_MAINNET', # added as batch 1 - 'ARBITRUM_MAINNET,OPTIMISM_MAINNET', # added as batch 1 - 'AVALANCHE_MAINNET,OPTIMISM_MAINNET', # added as batch 2 - 'AVALANCHE_MAINNET,ARBITRUM_MAINNET', # added as batch 2 - 'BASE_MAINNET,POLYGON_MAINNET', # added as batch 2 - 'BSC_MAINNET,OPTIMISM_MAINNET', # added as batch 2 - 'AVALANCHE_MAINNET,BASE_MAINNET', # added as batch 2 - 'WEMIX_MAINNET,KROMA_MAINNET', - 'BSC_MAINNET,WEMIX_MAINNET', # added as batch 2 - 'AVALANCHE_MAINNET,WEMIX_MAINNET', # added as batch 2 - 'POLYGON_MAINNET,WEMIX_MAINNET', # added as batch 2 - 'WEMIX_MAINNET,ARBITRUM_MAINNET', # added as batch 2 - 'OPTIMISM_MAINNET,WEMIX_MAINNET' # added as batch 2 + 'ARBITRUM_MAINNET,BSC_MAINNET', + 'ARBITRUM_MAINNET,OPTIMISM_MAINNET', + 'AVALANCHE_MAINNET,ARBITRUM_MAINNET', + 'AVALANCHE_MAINNET,BASE_MAINNET', + 'AVALANCHE_MAINNET,BSC_MAINNET', + 'AVALANCHE_MAINNET,OPTIMISM_MAINNET', + 'AVALANCHE_MAINNET,POLYGON_MAINNET', + 'AVALANCHE_MAINNET,WEMIX_MAINNET', + 'BASE_MAINNET,ARBITRUM_MAINNET', + 'BASE_MAINNET,BSC_MAINNET', + 'BASE_MAINNET,OPTIMISM_MAINNET', + 'BASE_MAINNET,POLYGON_MAINNET', + 'BLAST_MAINNET,ARBITRUM_MAINNET', + 'BLAST_MAINNET,BASE_MAINNET', + 'BLAST_MAINNET,BSC_MAINNET', + 'BSC_MAINNET,OPTIMISM_MAINNET', + 'BSC_MAINNET,POLYGON_MAINNET', + 'BSC_MAINNET,WEMIX_MAINNET', + 'ETHEREUM_MAINNET,ARBITRUM_MAINNET', + 'ETHEREUM_MAINNET,AVALANCHE_MAINNET', + 'ETHEREUM_MAINNET,BASE_MAINNET', + 'ETHEREUM_MAINNET,BLAST_MAINNET', + 'ETHEREUM_MAINNET,BSC_MAINNET', + 'ETHEREUM_MAINNET,CELO_MAINNET', + 'ETHEREUM_MAINNET,GNOSIS_MAINNET', + 'ETHEREUM_MAINNET,METIS_ANDROMEDA', + 'ETHEREUM_MAINNET,MODE_MAINNET', + 'ETHEREUM_MAINNET,OPTIMISM_MAINNET', + 'ETHEREUM_MAINNET,POLYGON_MAINNET', + 'ETHEREUM_MAINNET,WEMIX_MAINNET', + 'ETHEREUM_MAINNET,ZKSYNC_MAINNET', + 'GNOSIS_MAINNET,ARBITRUM_MAINNET', + 'GNOSIS_MAINNET,AVALANCHE_MAINNET', + 'GNOSIS_MAINNET,BASE_MAINNET', + 'GNOSIS_MAINNET,BSC_MAINNET', + 'GNOSIS_MAINNET,OPTIMISM_MAINNET', + 'GNOSIS_MAINNET,POLYGON_MAINNET', + 'METIS_ANDROMEDA,ARBITRUM_MAINNET', + 'MODE_MAINNET,ARBITRUM_MAINNET', + 'MODE_MAINNET,BASE_MAINNET', + 'MODE_MAINNET,BSC_MAINNET', + 'MODE_MAINNET,OPTIMISM_MAINNET', + 'OPTIMISM_MAINNET,POLYGON_MAINNET', + 'OPTIMISM_MAINNET,WEMIX_MAINNET', + 'POLYGON_MAINNET,ARBITRUM_MAINNET', + 'POLYGON_MAINNET,WEMIX_MAINNET', + 'WEMIX_MAINNET,ARBITRUM_MAINNET', + 'WEMIX_MAINNET,KROMA_MAINNET', + 'ZKSYNC_MAINNET,ARBITRUM_MAINNET' ] BiDirectionalLane = true -PhaseTimeout = '30m' +PhaseTimeout = '20m' ExistingDeployment = true +SkipRequestIfAnotherRequestTriggeredWithin = '40m' [CCIP.Groups.load.TokenConfig] NoOfTokensPerChain = 1 @@ -1137,7 +1165,7 @@ TimeUnit = '1h' TestDuration = '5h' TestRunName = 'Soak_test_mainnet' FailOnFirstErrorInLoad = true -SkipRequestIfAnotherRequestTriggeredWithin = '40m' + [[CCIP.Groups.load.LoadProfile.MsgProfile.MsgDetails]] MsgType = 'Data' @@ -1149,34 +1177,55 @@ AmountPerToken = 1 [CCIP.Groups.smoke] # these are all the valid network pairs NetworkPairs = [ - 'ETHEREUM_MAINNET,OPTIMISM_MAINNET', - 'ETHEREUM_MAINNET,AVALANCHE_MAINNET', - 'ETHEREUM_MAINNET,POLYGON_MAINNET', - 'ETHEREUM_MAINNET,BSC_MAINNET', - 'ETHEREUM_MAINNET,ARBITRUM_MAINNET', - 'ETHEREUM_MAINNET,BASE_MAINNET', - 'ETHEREUM_MAINNET,WEMIX_MAINNET', - 'AVALANCHE_MAINNET,POLYGON_MAINNET', - 'BASE_MAINNET,OPTIMISM_MAINNET', - 'BASE_MAINNET,ARBITRUM_MAINNET', - 'AVALANCHE_MAINNET,BSC_MAINNET', - 'BSC_MAINNET,POLYGON_MAINNET', - 'OPTIMISM_MAINNET,POLYGON_MAINNET', - 'BASE_MAINNET,BSC_MAINNET', - 'POLYGON_MAINNET,ARBITRUM_MAINNET', # added as batch 1 - 'ARBITRUM_MAINNET,BSC_MAINNET', # added as batch 1 - 'ARBITRUM_MAINNET,OPTIMISM_MAINNET', # added as batch 1 - 'AVALANCHE_MAINNET,OPTIMISM_MAINNET', # added as batch 2 - 'AVALANCHE_MAINNET,ARBITRUM_MAINNET', # added as batch 2 - 'BASE_MAINNET,POLYGON_MAINNET', # added as batch 2 - 'BSC_MAINNET,OPTIMISM_MAINNET', # added as batch 2 - 'AVALANCHE_MAINNET,BASE_MAINNET', # added as batch 2 - 'WEMIX_MAINNET,KROMA_MAINNET', - 'BSC_MAINNET,WEMIX_MAINNET', # added as batch 2 - 'AVALANCHE_MAINNET,WEMIX_MAINNET', # added as batch 2 - 'POLYGON_MAINNET,WEMIX_MAINNET', # added as batch 2 - 'WEMIX_MAINNET,ARBITRUM_MAINNET', # added as batch 2 - 'OPTIMISM_MAINNET,WEMIX_MAINNET' # added as batch 2 + 'ARBITRUM_MAINNET,BSC_MAINNET', + 'ARBITRUM_MAINNET,OPTIMISM_MAINNET', + 'AVALANCHE_MAINNET,ARBITRUM_MAINNET', + 'AVALANCHE_MAINNET,BASE_MAINNET', + 'AVALANCHE_MAINNET,BSC_MAINNET', + 'AVALANCHE_MAINNET,OPTIMISM_MAINNET', + 'AVALANCHE_MAINNET,POLYGON_MAINNET', + 'AVALANCHE_MAINNET,WEMIX_MAINNET', + 'BASE_MAINNET,ARBITRUM_MAINNET', + 'BASE_MAINNET,BSC_MAINNET', + 'BASE_MAINNET,OPTIMISM_MAINNET', + 'BASE_MAINNET,POLYGON_MAINNET', + 'BLAST_MAINNET,ARBITRUM_MAINNET', + 'BLAST_MAINNET,BASE_MAINNET', + 'BLAST_MAINNET,BSC_MAINNET', + 'BSC_MAINNET,OPTIMISM_MAINNET', + 'BSC_MAINNET,POLYGON_MAINNET', + 'BSC_MAINNET,WEMIX_MAINNET', + 'ETHEREUM_MAINNET,ARBITRUM_MAINNET', + 'ETHEREUM_MAINNET,AVALANCHE_MAINNET', + 'ETHEREUM_MAINNET,BASE_MAINNET', + 'ETHEREUM_MAINNET,BLAST_MAINNET', + 'ETHEREUM_MAINNET,BSC_MAINNET', + 'ETHEREUM_MAINNET,CELO_MAINNET', + 'ETHEREUM_MAINNET,GNOSIS_MAINNET', + 'ETHEREUM_MAINNET,METIS_ANDROMEDA', + 'ETHEREUM_MAINNET,MODE_MAINNET', + 'ETHEREUM_MAINNET,OPTIMISM_MAINNET', + 'ETHEREUM_MAINNET,POLYGON_MAINNET', + 'ETHEREUM_MAINNET,WEMIX_MAINNET', + 'ETHEREUM_MAINNET,ZKSYNC_MAINNET', + 'GNOSIS_MAINNET,ARBITRUM_MAINNET', + 'GNOSIS_MAINNET,AVALANCHE_MAINNET', + 'GNOSIS_MAINNET,BASE_MAINNET', + 'GNOSIS_MAINNET,BSC_MAINNET', + 'GNOSIS_MAINNET,OPTIMISM_MAINNET', + 'GNOSIS_MAINNET,POLYGON_MAINNET', + 'METIS_ANDROMEDA,ARBITRUM_MAINNET', + 'MODE_MAINNET,ARBITRUM_MAINNET', + 'MODE_MAINNET,BASE_MAINNET', + 'MODE_MAINNET,BSC_MAINNET', + 'MODE_MAINNET,OPTIMISM_MAINNET', + 'OPTIMISM_MAINNET,POLYGON_MAINNET', + 'OPTIMISM_MAINNET,WEMIX_MAINNET', + 'POLYGON_MAINNET,ARBITRUM_MAINNET', + 'POLYGON_MAINNET,WEMIX_MAINNET', + 'WEMIX_MAINNET,ARBITRUM_MAINNET', + 'WEMIX_MAINNET,KROMA_MAINNET', + 'ZKSYNC_MAINNET,ARBITRUM_MAINNET' ] BiDirectionalLane = true @@ -1184,7 +1233,7 @@ PhaseTimeout = '20m' LocalCluster = false ExistingDeployment = true ReuseContracts = true - +SkipRequestIfAnotherRequestTriggeredWithin = '24h' [CCIP.Groups.smoke.TokenConfig] NoOfTokensPerChain = 1