From 374482e40c533c4b8279bcb750db08ea66fc814a Mon Sep 17 00:00:00 2001 From: Balamurali Gopalswami <167726375+b-gopalswami@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:45:12 -0400 Subject: [PATCH 1/2] CCIP-3461: Optimize mainnet soak test (#1458) ### Motivation Optimize the resource and fund consumption of the mainnet soak test. Currently, the mainnet soak test runs every six hours in Kubernetes, executing one CCIP transaction per hour for a duration of five hours across 28 bidirectional lanes. This setup is designed to provide consistent observability data on the CCIP mainnet, allowing us to differentiate between service outages and quiet periods. However, these tests are inefficient, consuming Kubernetes resources and significant mainnet funds. https://smartcontract-it.atlassian.net/browse/CCIP-3461 ### Ideas: Discussion [thread](https://chainlink-core.slack.com/archives/C02PBKHCP28/p1726678958525029) is initiated with o11y team and decided that it's not required to create txs for every hour instead create one tx if there are no activity for last 24h. Convert to smoke test as we are planning to fire only one request instead of Soak test. Converting to smoke will elevate the K8 resource consumption as the test will run using github runner. ### Solution - [x] 1. Modify the pipeline to run it as smoke test - [x] 2. Add traffic check to smoke test - [x] 3. Add new set of additional 21 lanes and resulted in total of 49 lanes. (i.e 98 unique lanes) - [x] 5. Add RPCs, Wallet key for new lanes to test secrets - [ ] 6. Load funds for new lanes - [x] 7. Schedule pipeline to run for every 6hrs ### Key outcomes: Present transaction count: 1tx * (28 * 2)lanes * 24hrs = 1344 After this change: 1tx * (49 * 2)lanes = 98 which is close to 92% reduction with additional 42 lanes coverage. As per the last [analysis](https://docs.google.com/document/d/1MqXoIyGtGE9o2usgpWdIHQUMblKTyeAvoa06MEsExpc/edit?pli=1#heading=h.8kwk6enqa4w) on the cost, we spend around 84k per quarter. I expect the fund reduction close to 90% which will give saving close to 300k annually. --------- Co-authored-by: Brandon West <3317895+Bwest981@users.noreply.github.com> --- .github/workflows/ccip-live-network-tests.yml | 130 +++++++++--- .../ccip-tests/actions/ccip_helpers.go | 25 ++- .../ccip-tests/load/ccip_loadgen.go | 8 +- .../ccip-tests/load/ccip_multicall_loadgen.go | 2 +- integration-tests/ccip-tests/load/helper.go | 2 +- .../ccip-tests/smoke/ccip_test.go | 133 ++++++++++++- .../ccip-tests/testconfig/README.md | 12 +- .../ccip-tests/testconfig/ccip.go | 92 ++++----- .../testconfig/override/mainnet.toml | 187 +++++++++++------- 9 files changed, 425 insertions(+), 166 deletions(-) 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/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 From b71329d41b803ca16dc9a1f9d5247c87fd6fbc32 Mon Sep 17 00:00:00 2001 From: Patrick Date: Fri, 27 Sep 2024 09:52:37 -0400 Subject: [PATCH 2/2] Removing chain dependencies from NewCommitServices construct (#1361) ## Motivation LOOP-ify ## Solution --------- Co-authored-by: Bartek Tofel Co-authored-by: lukaszcl <120112546+lukaszcl@users.noreply.github.com> --- GNUmakefile | 4 + core/chainlink.debug.Dockerfile | 97 +++++++++++ core/services/ocr2/delegate.go | 92 +++++++++-- .../plugins/ccip/ccipcommit/initializers.go | 47 +----- .../ocr2/plugins/ccip/exportinternal.go | 21 ++- .../plugins/ccip/internal/pricegetter/evm.go | 152 ++++++++++++------ .../ccip/internal/pricegetter/evm_test.go | 118 +++++++++++++- .../ocr2/plugins/ccip/internal/rpclib/evm.go | 4 + integration-tests/ccip-tests/Makefile | 7 + 9 files changed, 422 insertions(+), 120 deletions(-) create mode 100644 core/chainlink.debug.Dockerfile 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/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) ../../