From 1a6885c0f816e2d095120d05534377ce9ba807fe Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Thu, 26 Mar 2026 17:50:23 +0100 Subject: [PATCH] use V2 PoR example --- .changeset/bright-pens-glow.md | 5 - .changeset/fresh-cakes-hit.md | 5 - ...1773776956.md => minor-bump-1774375762.md} | 0 .changeset/sour-sharks-aid.md | 5 - .changeset/vault-auth-gw-handler.md | 7 + .github/E2E_TESTS_ON_GITHUB_CI.md | 2 - .github/e2e-tests.yml | 84 - .github/workflows/build-publish.yml | 4 +- .github/workflows/ci-core.yml | 143 +- .../workflows/client-compatibility-tests.yml | 18 +- .github/workflows/codeql.yml | 6 +- .github/workflows/cre-local-env-tests.yaml | 7 +- .../cre-regression-system-tests.yaml | 12 +- .github/workflows/cre-soak-memory-leak.yml | 5 +- .github/workflows/cre-system-tests.yaml | 12 +- .github/workflows/docker-build.yml | 27 +- .github/workflows/go-mod-cache.yml | 6 +- .../workflows/integration-in-memory-tests.yml | 76 +- .github/workflows/integration-tests.yml | 6 +- .../workflows/on-demand-vrfv2-smoke-tests.yml | 107 -- .github/workflows/system-tests-nightly.yml | 25 +- CHANGELOG.md | 14 + GNUmakefile | 12 +- .../executable/request/server_request.go | 5 + .../executable/request/server_request_test.go | 36 + core/capabilities/remote/executable/server.go | 2 + .../remote/executable/server_test.go | 106 ++ core/capabilities/vault/capability.go | 144 -- core/capabilities/vault/capability_test.go | 46 +- .../capabilities/vault/digest_replay_guard.go | 63 + .../vault/digest_replay_guard_test.go | 183 +++ core/capabilities/vault/gw_handler.go | 245 ++- core/capabilities/vault/gw_handler_test.go | 163 +- core/capabilities/vault/request_authorizer.go | 42 +- core/capabilities/vault/vaulttypes/types.go | 30 +- core/chainlink.Dockerfile | 4 + core/cmd/shell.go | 7 + core/config/env/env.go | 1 + core/internal/testutils/testutils.go | 9 - core/scripts/chaincli/README.md | 4 +- core/scripts/cre/environment/README.md | 51 +- core/scripts/cre/environment/completions.go | 3 +- .../environment/environment/environment.go | 16 +- .../cre/environment/environment/examples.go | 173 +- .../environment/state_resolver_test.go | 146 -- .../examples/pkg/deploy/consumer.go | 73 + .../proof-of-reserve/web-trigger-based/go.mod | 91 - .../proof-of-reserve/web-trigger-based/go.sum | 213 --- .../web-trigger-based/main.go | 171 -- .../web-trigger-based/types/types.go | 16 - core/scripts/go.mod | 17 +- core/scripts/go.sum | 18 +- core/services/chainlink/application.go | 4 +- core/services/chainlink/node_platform.go | 136 ++ core/services/chainlink/node_platform_test.go | 80 + .../gateway/handlers/vault/aggregator.go | 5 - .../gateway/handlers/vault/aggregator_test.go | 12 +- .../gateway/handlers/vault/handler.go | 5 +- .../services/llo/bm/dummy_transmitter_test.go | 11 +- .../channel_definition_cache_factory_test.go | 5 +- .../onchain_channel_definition_cache.go | 16 +- .../onchain_channel_definition_cache_test.go | 3 +- .../static_channel_definitions_cache.go | 3 +- core/services/llo/cleanup_test.go | 19 +- core/services/llo/cre/transmitter_test.go | 6 +- core/services/llo/keyring.go | 4 +- core/services/llo/keyring_test.go | 17 +- .../llo/mercurytransmitter/orm_test.go | 3 +- .../persistence_manager_test.go | 19 +- .../llo/mercurytransmitter/queue_test.go | 8 +- .../services/llo/mercurytransmitter/server.go | 6 +- .../mercurytransmitter/transmitter_test.go | 20 +- core/services/llo/orm_test.go | 4 +- core/services/llo/report_codecs_test.go | 4 +- .../retirement_report_cache_test.go | 4 +- core/services/ocr2/delegate.go | 5 +- core/services/ocr2/plugins/vault/plugin.go | 30 +- .../ocr2/plugins/vault/plugin_test.go | 81 + core/utils/utils.go | 8 - core/web/testdata/body/health.html | 4 +- core/web/testdata/body/health.json | 9 + core/web/testdata/body/health.txt | 1 + .../jobs/operations/propose_gateway_job.go | 22 +- .../operations/propose_gateway_job_test.go | 4 +- deployment/cre/jobs/pkg/gateway_job.go | 9 +- deployment/cre/jobs/pkg/gateway_job_test.go | 6 + deployment/cre/jobs/pkg/nodes.go | 4 + deployment/cre/jobs/pkg/types.go | 17 + deployment/cre/jobs/propose_job_spec_test.go | 131 ++ deployment/cre/pkg/offchain/nodes.go | 4 + deployment/go.mod | 9 +- deployment/go.sum | 18 +- devenv/contracts/vrf_v2.go | 699 ++++++++ devenv/env-vrfv2-bhs.toml | 28 + devenv/env-vrfv2.toml | 23 + devenv/environment.go | 3 + devenv/go.mod | 4 +- devenv/products/evm.go | 79 + devenv/products/vrfv2/basic.toml | 54 + devenv/products/vrfv2/bhs.toml | 58 + devenv/products/vrfv2/configuration.go | 121 ++ devenv/products/vrfv2/core.go | 454 +++++ devenv/products/vrfv2/job_spec.go | 118 ++ devenv/products/vrfv2/job_spec_bhs.go | 36 + devenv/products/vrfv2/two_keys.toml | 54 + devenv/tests/logpoller/config.go | 146 ++ devenv/tests/logpoller/logpoller_test.go | 138 -- devenv/tests/vrfv2/batch_test.go | 223 +++ devenv/tests/vrfv2/bhs_test.go | 194 +++ devenv/tests/vrfv2/helpers.go | 187 +++ devenv/tests/vrfv2/multiple_keys_test.go | 87 + devenv/tests/vrfv2/smoke_test.go | 291 ++++ devenv/tests/vrfv2plus/bhf_test.go | 2 +- devenv/tests/vrfv2plus/bhs_test.go | 78 +- go.md | 10 +- go.mod | 8 +- go.sum | 16 +- integration-tests/Makefile | 2 +- integration-tests/ccip-tests/Makefile | 4 +- integration-tests/docker/README.md | 3 +- integration-tests/go.mod | 10 +- integration-tests/go.sum | 16 +- integration-tests/load/go.mod | 8 +- integration-tests/load/go.sum | 16 +- integration-tests/smoke/README.md | 75 +- integration-tests/smoke/vrfv2_test.go | 1463 ----------------- package.json | 2 +- plugins/chainlink.Dockerfile | 4 + plugins/plugins.private.yaml | 2 +- plugins/plugins.public.yaml | 2 +- system-tests/lib/cre/features/vault/vault.go | 19 +- system-tests/lib/go.mod | 8 +- system-tests/lib/go.sum | 18 +- system-tests/tests/go.mod | 11 +- system-tests/tests/go.sum | 18 +- .../tests/smoke/cre/v2_vault_don_test.go | 289 +++- .../smoke/cre/v2_vault_don_test_helpers.go | 61 +- .../smoke/cre/vaultsecret/config/config.go | 7 + .../tests/smoke/cre/vaultsecret/go.mod | 22 + .../tests/smoke/cre/vaultsecret/go.sum | 37 + .../tests/smoke/cre/vaultsecret/main.go | 76 + system-tests/tests/test-helpers/t_helpers.go | 10 +- testdata/scripts/health/default.txtar | 10 + .../scripts/health/multi-chain-loopp.txtar | 10 + testdata/scripts/health/multi-chain.txtar | 10 + tools/docker/docker-compose.yaml | 5 +- 146 files changed, 5129 insertions(+), 3622 deletions(-) delete mode 100644 .changeset/bright-pens-glow.md delete mode 100644 .changeset/fresh-cakes-hit.md rename .changeset/{minor-bump-1773776956.md => minor-bump-1774375762.md} (100%) delete mode 100644 .changeset/sour-sharks-aid.md create mode 100644 .changeset/vault-auth-gw-handler.md delete mode 100644 .github/workflows/on-demand-vrfv2-smoke-tests.yml create mode 100644 core/capabilities/vault/digest_replay_guard.go create mode 100644 core/capabilities/vault/digest_replay_guard_test.go delete mode 100644 core/scripts/cre/environment/environment/state_resolver_test.go delete mode 100644 core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/go.mod delete mode 100644 core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/go.sum delete mode 100644 core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/main.go delete mode 100644 core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/types/types.go create mode 100644 core/services/chainlink/node_platform.go create mode 100644 core/services/chainlink/node_platform_test.go create mode 100644 devenv/contracts/vrf_v2.go create mode 100644 devenv/env-vrfv2-bhs.toml create mode 100644 devenv/env-vrfv2.toml create mode 100644 devenv/products/vrfv2/basic.toml create mode 100644 devenv/products/vrfv2/bhs.toml create mode 100644 devenv/products/vrfv2/configuration.go create mode 100644 devenv/products/vrfv2/core.go create mode 100644 devenv/products/vrfv2/job_spec.go create mode 100644 devenv/products/vrfv2/job_spec_bhs.go create mode 100644 devenv/products/vrfv2/two_keys.toml create mode 100644 devenv/tests/logpoller/config.go create mode 100644 devenv/tests/vrfv2/batch_test.go create mode 100644 devenv/tests/vrfv2/bhs_test.go create mode 100644 devenv/tests/vrfv2/helpers.go create mode 100644 devenv/tests/vrfv2/multiple_keys_test.go create mode 100644 devenv/tests/vrfv2/smoke_test.go delete mode 100644 integration-tests/smoke/vrfv2_test.go create mode 100644 system-tests/tests/smoke/cre/vaultsecret/config/config.go create mode 100644 system-tests/tests/smoke/cre/vaultsecret/go.mod create mode 100644 system-tests/tests/smoke/cre/vaultsecret/go.sum create mode 100644 system-tests/tests/smoke/cre/vaultsecret/main.go diff --git a/.changeset/bright-pens-glow.md b/.changeset/bright-pens-glow.md deleted file mode 100644 index d6abd06700b..00000000000 --- a/.changeset/bright-pens-glow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#added Add ListPendingJobProposals and ApproveJobProposalByID to the GQL SDK Client diff --git a/.changeset/fresh-cakes-hit.md b/.changeset/fresh-cakes-hit.md deleted file mode 100644 index 6bbb4ecfc65..00000000000 --- a/.changeset/fresh-cakes-hit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#changed Bumps the chainlink-ccip reference and replaces all references to `latest` with version-locked imports diff --git a/.changeset/minor-bump-1773776956.md b/.changeset/minor-bump-1774375762.md similarity index 100% rename from .changeset/minor-bump-1773776956.md rename to .changeset/minor-bump-1774375762.md diff --git a/.changeset/sour-sharks-aid.md b/.changeset/sour-sharks-aid.md deleted file mode 100644 index 2ff7fe12302..00000000000 --- a/.changeset/sour-sharks-aid.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#added Emit gas-related metrics through Beholder diff --git a/.changeset/vault-auth-gw-handler.md b/.changeset/vault-auth-gw-handler.md new file mode 100644 index 00000000000..5834309ebf9 --- /dev/null +++ b/.changeset/vault-auth-gw-handler.md @@ -0,0 +1,7 @@ +--- +"chainlink": patch +--- + +#changed + +Move Vault node-side request authorization into the gateway handler and remove duplicated authorization from the Vault capability. diff --git a/.github/E2E_TESTS_ON_GITHUB_CI.md b/.github/E2E_TESTS_ON_GITHUB_CI.md index 26d3c9daf92..1bdb8bb10ff 100644 --- a/.github/E2E_TESTS_ON_GITHUB_CI.md +++ b/.github/E2E_TESTS_ON_GITHUB_CI.md @@ -55,8 +55,6 @@ These are dispatched parametrized workflows, that may be triggered manually for - [On-Demand Automation Tests](https://github.com/smartcontractkit/chainlink/actions/workflows/automation-ondemand-tests.yml) - [CCIP Chaos Tests](https://github.com/smartcontractkit/chainlink/actions/workflows/ccip-chaos-tests.yml) - [CCIP Load Tests](https://github.com/smartcontractkit/chainlink/actions/workflows/ccip-load-tests.yml) -- [VRFv2Plus Smoke Tests](https://github.com/smartcontractkit/chainlink/actions/workflows/on-demand-vrfv2plus-smoke-tests.yml) -- [VRFv2Plus Performance Tests](https://github.com/smartcontractkit/chainlink/actions/workflows/on-demand-vrfv2plus-performance-test.yml) ### Test workflows setup in CI diff --git a/.github/e2e-tests.yml b/.github/e2e-tests.yml index 77b7b33c75f..610c6a025e1 100644 --- a/.github/e2e-tests.yml +++ b/.github/e2e-tests.yml @@ -6,90 +6,6 @@ # - The triggers (e.g., Run PR E2E Tests, Nightly E2E Tests) that should trigger these tests. # runner-test-matrix: - # START: OCR tests - - # All the OCR smoke/soak/chaostests were moved to https://github.com/smartcontractkit/chainlink/blob/develop/.github/workflows/devenv-ocr2-soak.yml - # and https://github.com/smartcontractkit/chainlink/blob/develop/.github/workflows/devenv-ocr2-chaos.yml - - # START: VRF tests - - - id: smoke/vrfv2_test.go:TestVRFv2Basic - path: integration-tests/smoke/vrfv2_test.go - runs_on: ubuntu22.04-8cores-32GB - test_env_type: docker - test_cmd: | - cd smoke && \ - gotestsum \ - --junitfile=/tmp/junit.xml \ - --jsonfile=/tmp/gotest.log \ - --format=github-actions \ - -- -v -run "TestVRFv2Basic" -parallel=1 -timeout 30m -count=1 - test_secrets_required: true - triggers: - - On Demand VRFV2 Smoke Test (Ethereum clients) - test_go_project_path: integration-tests - - - id: smoke/vrf_test.go:* - path: integration-tests/smoke/vrf_test.go - test_env_type: docker - runs_on: ubuntu-latest - triggers: - - PR E2E Core Tests - - Merge Queue E2E Core Tests - - Nightly E2E Tests - - Push E2E Core Tests - - Workflow Dispatch E2E Core Tests - test_cmd: | - gotestsum \ - --junitfile=/tmp/junit.xml \ - --jsonfile=/tmp/gotest.log \ - --format=github-actions \ - -- -v -run "TestVRFBasic|TestVRFJobReplacement" -timeout 30m -count=1 -parallel=2 github.com/smartcontractkit/chainlink/integration-tests/smoke - pyroscope_env: ci-smoke-vrf-evm-simulated - test_go_project_path: integration-tests - - - id: smoke/vrfv2_test.go:* - path: integration-tests/smoke/vrfv2_test.go - test_env_type: docker - runs_on: ubuntu-latest - triggers: - - PR E2E Core Tests - - Merge Queue E2E Core Tests - - Nightly E2E Tests - - Push E2E Core Tests - - Workflow Dispatch E2E Core Tests - test_cmd: | - gotestsum \ - --junitfile=/tmp/junit.xml \ - --jsonfile=/tmp/gotest.log \ - --format=github-actions \ - -- -v -run "^(TestVRFv2Basic|TestVRFV2WithBHS|TestVRFv2NodeReorg|TestVRFV2MultipleSendingKeys|TestVRFOwner|TestVRFV2BatchFulfillmentEnabledDisabled)$" -timeout 30m -count=1 -parallel=6 github.com/smartcontractkit/chainlink/integration-tests/smoke - pyroscope_env: ci-smoke-vrf2-evm-simulated - test_go_project_path: integration-tests - # END: VRF tests - - # START: Other tests - - - id: smoke/reorg_above_finality_test.go:* - path: integration-tests/smoke/reorg_above_finality_test.go - test_env_type: docker - runs_on: ubuntu-latest - triggers: - - PR E2E Core Tests - - Merge Queue E2E Core Tests - - Nightly E2E Tests - - Push E2E Core Tests - - Workflow Dispatch E2E Core Tests - test_cmd: | - gotestsum \ - --junitfile=/tmp/junit.xml \ - --jsonfile=/tmp/gotest.log \ - --format=github-actions \ - -- -v -run "TestRegisteringMultipleJobDistributor" -timeout 30m -count=1 -parallel=2 github.com/smartcontractkit/chainlink/integration-tests/smoke - pyroscope_env: ci-smoke-reorg-above-finality-evm-simulated - test_go_project_path: integration-tests - # END: Other tests - # START: CCIPv1.6 tests - id: smoke/ccip/ccip_reorg_test.go:LessThanFinalityTests diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml index 864ea4fa950..d52b9f747e8 100644 --- a/.github/workflows/build-publish.yml +++ b/.github/workflows/build-publish.yml @@ -96,7 +96,7 @@ jobs: permissions: contents: read id-token: write - uses: smartcontractkit/.github/.github/workflows/reusable-docker-build-publish.yml@a89ebdd9d9cabda77c5f7ea7523af5707afb7786 # 2025-08-25 + uses: smartcontractkit/.github/.github/workflows/reusable-docker-build-publish.yml@f4ff50d0f4713ed7b247dbd8a58316484907f958 # 2026-01-13 with: aws-ecr-name: chainlink aws-region-ecr: us-east-1 @@ -131,7 +131,7 @@ jobs: permissions: contents: read id-token: write - uses: smartcontractkit/.github/.github/workflows/reusable-docker-build-publish.yml@a89ebdd9d9cabda77c5f7ea7523af5707afb7786 # 2025-08-25 + uses: smartcontractkit/.github/.github/workflows/reusable-docker-build-publish.yml@f4ff50d0f4713ed7b247dbd8a58316484907f958 # 2026-01-13 with: aws-ecr-name: ccip aws-region-ecr: us-east-1 diff --git a/.github/workflows/ci-core.yml b/.github/workflows/ci-core.yml index 912faa7e807..646d7596646 100644 --- a/.github/workflows/ci-core.yml +++ b/.github/workflows/ci-core.yml @@ -112,25 +112,25 @@ jobs: golangci: name: GolangCI Lint - needs: [filter, run-frequency, runner-config] + needs: [filter, run-frequency ] # We don't directly merge dependabot PRs to not waste the resources. - if: ${{ (github.event_name == 'pull_request' || github.event_name == 'schedule') && github.actor != 'dependabot[bot]' }} + if: ${{ needs.filter.outputs.affected-modules != '[]' && github.event_name != 'merge_group' && github.actor != 'dependabot[bot]' }} permissions: # To annotate code in the PR. checks: write contents: read # For golangci-lint-action's `only-new-issues` option. pull-requests: read - runs-on: ${{ needs.runner-config.outputs.lint-runner }} + runs-on: runs-on=${{ github.run_id }}-${{ strategy.job-index }}/cpu=16/ram=32/family=c6gd/spot=false/image=ubuntu24-full-arm64/extras=s3-cache strategy: fail-fast: false matrix: modules: ${{ fromJson(needs.filter.outputs.affected-modules) }} steps: - name: Enable S3 Cache for Self-Hosted Runners - # these env vars are set (and exposed) when it is a self-hosted runner with extras=s3-cache - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Checkout uses: actions/checkout@v4 @@ -206,27 +206,27 @@ jobs: matrix: type: - cmd: go_core_tests - os: ${{ needs.runner-config.outputs.core-tests-runner }} + os: runs-on=${{ github.run_id }}-unit/cpu=48/ram=96/family=c6i/spot=false/image=ubuntu24-full-x64/extras=s3-cache+tmpfs should-run: ${{ needs.filter.outputs.should-run-core-tests }} trunk-auto-quarantine: "true" - cmd: go_core_tests_integration - os: ${{ needs.runner-config.outputs.core-tests-integration-runner }} + os: runs-on=${{ github.run_id }}-integ/cpu=48/ram=96/family=c6i/spot=false/image=ubuntu24-full-x64/extras=s3-cache+tmpfs should-run: ${{ needs.filter.outputs.should-run-core-tests }} trunk-auto-quarantine: "true" setup-solana: "true" install-loopps: "true" - cmd: go_core_fuzz - os: ${{ needs.runner-config.outputs.core-fuzz-tests-runner }} + os: runs-on=${{ github.run_id}}-fuzz/cpu=8/ram=32/family=m6id+m6idn/spot=false/image=ubuntu24-full-x64/extras=s3-cache should-run: ${{ needs.filter.outputs.should-run-core-tests }} - cmd: go_core_race_tests - os: ${{ needs.runner-config.outputs.core-race-tests-runner }} + os: runs-on=${{ github.run_id}}-race/cpu=64/ram=128/family=c7i/volume=80gb/spot=false/image=ubuntu24-full-x64/extras=s3-cache should-run: ${{ needs.filter.outputs.should-run-core-tests }} - cmd: go_core_ccip_deployment_tests - os: ${{ needs.runner-config.outputs.deployment-tests-runner }} + os: runs-on=${{ github.run_id }}-deployment/cpu=64/ram=128/family=c6i+c7i/spot=false/image=ubuntu24-full-x64/extras=s3-cache+tmpfs should-run: ${{ needs.filter.outputs.should-run-deployment-tests }} trunk-auto-quarantine: "true" go-mod-directory: "deployment/" @@ -238,18 +238,19 @@ jobs: name: Core Tests (${{ matrix.type.cmd }}) # Be careful modifying the job name, as it is used to fetch the job URL # We don't directly merge dependabot PRs, so let's not waste the resources if: ${{ github.actor != 'dependabot[bot]' }} - needs: [filter, run-frequency, runner-config] + needs: [filter, run-frequency ] timeout-minutes: 60 - runs-on: ${{ matrix.type.os }} + # Use ubuntu-latest for jobs that will be skipped + runs-on: ${{ matrix.type.should-run == 'true' && matrix.type.os || 'ubuntu-latest' }} permissions: id-token: write contents: read actions: read steps: - name: Enable S3 Cache for Self-Hosted Runners - # these env vars are set (and exposed) when it is a self-hosted runner with extras=s3-cache - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Checkout the repo if: ${{ matrix.type.should-run == 'true' }} @@ -598,116 +599,6 @@ jobs: echo "one-per-day-frequency=true" | tee -a $GITHUB_OUTPUT fi - # This chooses which runner labels we pass for the matrix jobs above. - # General Criteria: - # 1. If we are going to 'skip' a test suite, we use the base Github-hosted runner. - # - This is based off `should-run-core-tests`, and `should-run-deployment-tests` - # 2. If we are not skipping, we check if the PR has the "runs-on-opt-out" label. - # - If the PR has the label, we use the larger Github-hosted runners. - # - If the PR does not have the label, we use the self-hosted runners. - runner-config: - name: Runner Config - needs: [filter] - runs-on: ubuntu-latest - env: - # include unique label (core/deployment/etc...) to ensure jobs are not competing for the same runner - SH_TEST_RUNNER: runs-on=${{ github.run_id }}-core/cpu=48/ram=96/family=c6i/spot=false/image=ubuntu24-full-x64/extras=s3-cache+tmpfs - SH_DEPLOYMENT_TEST_RUNNER: runs-on=${{ github.run_id }}-deployment/cpu=48/ram=96/family=c6id/spot=false/image=ubuntu24-full-x64/extras=s3-cache - SH_FUZZ_RUNNER: runs-on=${{ github.run_id}}-fuzz/cpu=8+16/ram=32+64/family=c6id+m6id+m6idn/spot=false/image=ubuntu24-full-x64/extras=s3-cache - SH_RACE_TEST_RUNNER: runs-on=${{ github.run_id}}-race/cpu=64+128/ram=128+128/family=c7+m7/volume=80gb/spot=false/image=ubuntu24-full-x64/extras=s3-cache - SH_LINT_RUNNER: runs-on=${{ github.run_id }}-lint/cpu=16/ram=32/family=c6gd/spot=false/image=ubuntu24-full-arm64/extras=s3-cache - GH_TEST_RUNNER: ubuntu22.04-32cores-128GB - GH_FUZZ_RUNNER: ubuntu22.04-8cores-32GB - GH_BASE_RUNNER: ubuntu-latest - GH_LINT_RUNNER: ubuntu-24.04-8cores-32GB-ARM - outputs: - # go_core_tests / go_core_race_tests / go_core_tests_integration - core-tests-runner: ${{ steps.core-tests.outputs.core-tests-runner }} - core-tests-integration-runner: ${{ steps.core-tests.outputs.core-tests-integration-runner }} - core-fuzz-tests-runner: ${{ steps.core-tests.outputs.core-fuzz-tests-runner }} - core-race-tests-runner: ${{ steps.core-tests.outputs.core-race-tests-runner }} - # go_core_ccip_deployment_tests - deployment-tests-runner: ${{ steps.deployment-tests.outputs.deployment-tests-runner }} - # linting - lint-runner: ${{ steps.linting.outputs.lint-runner }} - steps: - - name: Get PR Labels - id: pr-labels - uses: smartcontractkit/.github/actions/get-pr-labels@get-pr-labels/v1 - with: - check-label: "runs-on-opt-out" - - - name: Select runners for deployment tests - id: deployment-tests - shell: bash - env: - OPT_OUT: ${{ steps.pr-labels.outputs.check-label-found || 'false' }} - SHOULD_RUN_DEPLOYMENT_TESTS: ${{ needs.filter.outputs.should-run-deployment-tests }} - run: | - if [[ "${SHOULD_RUN_DEPLOYMENT_TESTS}" == "false" ]]; then - echo "Deployment tests will be skipped, using base Github-hosted runner." - echo "deployment-tests-runner=${GH_BASE_RUNNER}" | tee -a $GITHUB_OUTPUT - exit 0 - fi - - if [[ "$OPT_OUT" == "true" ]]; then - echo "Opt-out is true for current run. Using gh-hosted runner for deployment tests." - echo "deployment-tests-runner=${GH_TEST_RUNNER}" | tee -a $GITHUB_OUTPUT - exit 0 - fi - - echo "Opt-out is false for current run. Using self-hosted runner for deployment tests." - echo "deployment-tests-runner=${SH_DEPLOYMENT_TEST_RUNNER}" | tee -a $GITHUB_OUTPUT - - - name: Select runners for core tests - id: core-tests - shell: bash - env: - OPT_OUT: ${{ steps.pr-labels.outputs.check-label-found || 'false' }} - SHOULD_RUN_CORE_TESTS: ${{ needs.filter.outputs.should-run-core-tests }} - run: | - if [[ "${SHOULD_RUN_CORE_TESTS}" == "false" ]]; then - echo "Core tests will be skipped, using base Github-hosted runner." - - echo "core-tests-runner=${GH_BASE_RUNNER}" | tee -a $GITHUB_OUTPUT - echo "core-tests-integration-runner=${GH_BASE_RUNNER}" | tee -a $GITHUB_OUTPUT - echo "core-fuzz-tests-runner=${GH_BASE_RUNNER}" | tee -a $GITHUB_OUTPUT - echo "core-race-tests-runner=${GH_BASE_RUNNER}" | tee -a $GITHUB_OUTPUT - exit 0 - fi - - if [[ "$OPT_OUT" == "true" ]]; then - echo "Opt-out is true for current run. Using gh-hosted runner for core tests." - - echo "core-tests-runner=${GH_TEST_RUNNER}" | tee -a $GITHUB_OUTPUT - echo "core-tests-integration-runner=${GH_TEST_RUNNER}" | tee -a $GITHUB_OUTPUT - echo "core-fuzz-tests-runner=${GH_FUZZ_RUNNER}" | tee -a $GITHUB_OUTPUT - echo "core-race-tests-runner=${GH_TEST_RUNNER}" | tee -a $GITHUB_OUTPUT - exit 0 - fi - - echo "Opt-out is false for current run. Using self-hosted runner for core tests." - - echo "core-tests-runner=${SH_TEST_RUNNER}" | tee -a $GITHUB_OUTPUT - echo "core-tests-integration-runner=${SH_TEST_RUNNER}" | tee -a $GITHUB_OUTPUT - echo "core-fuzz-tests-runner=${SH_FUZZ_RUNNER}" | tee -a $GITHUB_OUTPUT - echo "core-race-tests-runner=${SH_RACE_TEST_RUNNER}" | tee -a $GITHUB_OUTPUT - - - name: Select runners for linting - id: linting - shell: bash - env: - OPT_OUT: ${{ steps.pr-labels.outputs.check-label-found || 'false' }} - run: | - if [[ "$OPT_OUT" == "true" ]]; then - echo "Opt-out is true for current run. Using gh-hosted runner for linting." - echo "lint-runner=${GH_LINT_RUNNER}" | tee -a $GITHUB_OUTPUT - exit 0 - fi - - echo "Opt-out is false for current run. Using self-hosted runner for linting." - echo "lint-runner=${SH_LINT_RUNNER}" | tee -a $GITHUB_OUTPUT - misc: # Catchall job for miscellaneous steps. name: Misc diff --git a/.github/workflows/client-compatibility-tests.yml b/.github/workflows/client-compatibility-tests.yml index 4b483d510ba..1b32e69a5ae 100644 --- a/.github/workflows/client-compatibility-tests.yml +++ b/.github/workflows/client-compatibility-tests.yml @@ -349,7 +349,7 @@ jobs: persist-credentials: false ref: ${{ needs.select-versions.outputs.chainlink_version }} - name: Build Chainlink Image - uses: smartcontractkit/.github/actions/ctf-build-image@b7be49d6f54ce0990354931ee1e4c8624f3f406e # v1.4.0 (not released yet) + uses: smartcontractkit/.github/actions/ctf-build-image@ctf-build-image/v1 with: image-tag: ${{ inputs.chainlinkVersion || needs.select-versions.outputs.chainlink_image_version || github.sha }} dockerfile: core/chainlink.Dockerfile @@ -481,10 +481,10 @@ jobs: { "name": "evm-implementation-compatibility-test-68", "os": "ubuntu-latest", - "product": "vrfv2", + "product": "vrf", "eth_implementation": "erigon", "docker_image": "thorax/erigon:v2.59.2", - "run": "-run '^TestVRFv2Basic/Request_Randomness$' ./smoke/vrfv2_test.go" + "run": "-run '^TestVRFBasic/Request_Randomness$' ./smoke/vrf_test.go" } ] EOF @@ -507,7 +507,6 @@ jobs: if [[ "$ETH_IMPLEMENTATIONS" == *"geth"* ]]; then echo "Will test compatibility with geth" testlistgenerator -o compatibility_test_list.json -p vrf -r '^TestVRFBasic/Request_Randomness$' -f './smoke/vrf_test.go' -e geth -d "${{ needs.get-latest-available-images.outputs.geth_images }}" -t "evm-implementation-compatibility-test" -n "ubuntu-latest" - testlistgenerator -o compatibility_test_list.json -p vrfv2 -r '^TestVRFv2Basic/Request_Randomness$' -f './smoke/vrfv2_test.go' -e geth -d "${{ needs.get-latest-available-images.outputs.geth_images }}" -t "evm-implementation-compatibility-test" -n "ubuntu-latest" else echo "Will not test compatibility with geth" fi @@ -515,8 +514,7 @@ jobs: if [[ "$ETH_IMPLEMENTATIONS" == *"besu"* ]]; then echo "Will test compatibility with besu" testlistgenerator -o compatibility_test_list.json -p vrf -r '^TestVRFBasic/Request_Randomness$' -f './smoke/vrf_test.go' -e besu -d "${{ needs.get-latest-available-images.outputs.besu_images }}" -t "evm-implementation-compatibility-test" -n "ubuntu-latest" - # VRFv2 and VRFV2Plus tests are disabled for besu until the functionalities they rely on are supported - # testlistgenerator -o compatibility_test_list.json -p vrfv2 -r '^TestVRFv2Basic/Request_Randomness$' -f './smoke/vrfv2_test.go' -e besu -d "${{ needs.get-latest-available-images.outputs.besu_images }}" -t "evm-implementation-compatibility-test" -n "ubuntu-latest" + # VRFV2Plus tests are disabled for besu until the functionalities they rely on are supported else echo "Will not test compatibility with besu" fi @@ -524,7 +522,6 @@ jobs: if [[ "$ETH_IMPLEMENTATIONS" == *"erigon"* ]]; then echo "Will test compatibility with erigon" testlistgenerator -o compatibility_test_list.json -p vrf -r '^TestVRFBasic/Request_Randomness$' -f './smoke/vrf_test.go' -e erigon -d "${{ needs.get-latest-available-images.outputs.erigon_images }}" -t "evm-implementation-compatibility-test" -n "ubuntu-latest" - testlistgenerator -o compatibility_test_list.json -p vrfv2 -r '^TestVRFv2Basic/Request_Randomness$' -f './smoke/vrfv2_test.go' -e erigon -d "${{ needs.get-latest-available-images.outputs.erigon_images }}" -t "evm-implementation-compatibility-test" -n "ubuntu-latest" else echo "Will not test compatibility with erigon" fi @@ -532,8 +529,7 @@ jobs: if [[ "$ETH_IMPLEMENTATIONS" == *"nethermind"* ]]; then echo "Will test compatibility with nethermind" testlistgenerator -o compatibility_test_list.json -p vrf -r '^TestVRFBasic/Request_Randomness$' -f './smoke/vrf_test.go' -e nethermind -d "${{ needs.get-latest-available-images.outputs.nethermind_images }}" -t "evm-implementation-compatibility-test" -n "ubuntu-latest" - # VRFv2 and VRFV2Plus tests are disabled for nethermind until the functionalities they rely on are supported - # testlistgenerator -o compatibility_test_list.json -p vrfv2 -r '^TestVRFv2Basic/Request_Randomness$' -f './smoke/vrfv2_test.go' -e nethermind -d "${{ needs.get-latest-available-images.outputs.nethermind_images }}" -t "evm-implementation-compatibility-test" -n "ubuntu-latest" + # VRFV2Plus tests are disabled for nethermind until the functionalities they rely on are supported else echo "Will not test compatibility with nethermind" fi @@ -541,7 +537,6 @@ jobs: if [[ "$ETH_IMPLEMENTATIONS" == *"reth"* ]]; then echo "Will test compatibility with reth" testlistgenerator -o compatibility_test_list.json -p vrf -r '^TestVRFBasic/Request_Randomness$' -f './smoke/vrf_test.go' -e reth -d "${{ needs.get-latest-available-images.outputs.reth_images }}" -t "evm-implementation-compatibility-test" -n "ubuntu-latest" - testlistgenerator -o compatibility_test_list.json -p vrfv2 -r '^TestVRFv2Basic/Request_Randomness$' -f './smoke/vrfv2_test.go' -e reth -d "${{ needs.get-latest-available-images.outputs.reth_images }}" -t "evm-implementation-compatibility-test" -n "ubuntu-latest" else echo "Will not test compatibility with reth" fi @@ -770,7 +765,6 @@ jobs: workflowresultparser -workflowRunID ${{ github.run_id }} -githubToken ${{ github.token }} -githubRepo "${{ github.repository }}" -jobNameRegex "^ocr compatibility with (.*?)$" -namedKey="ocr" -outputFile=output.json workflowresultparser -workflowRunID ${{ github.run_id }} -githubToken ${{ github.token }} -githubRepo "${{ github.repository }}" -jobNameRegex "^ocr2 compatibility with (.*?)$" -namedKey="ocr2" -outputFile=output.json workflowresultparser -workflowRunID ${{ github.run_id }} -githubToken ${{ github.token }} -githubRepo "${{ github.repository }}" -jobNameRegex "^vrf compatibility with (.*?)$" -namedKey="vrf" -outputFile=output.json - workflowresultparser -workflowRunID ${{ github.run_id }} -githubToken ${{ github.token }} -githubRepo "${{ github.repository }}" -jobNameRegex "^vrfv2 compatibility with (.*?)$" -namedKey="vrfv2" -outputFile=output.json workflowresultparser -workflowRunID ${{ github.run_id }} -githubToken ${{ github.token }} -githubRepo "${{ github.repository }}" -jobNameRegex "^vrfv2plus compatibility with (.*?)$" -namedKey="vrfv2plus" -outputFile=output.json workflowresultparser -workflowRunID ${{ github.run_id }} -githubToken ${{ github.token }} -githubRepo "${{ github.repository }}" -jobNameRegex "^flux compatibility with (.*?)$" -namedKey="flux" -outputFile=output.json workflowresultparser -workflowRunID ${{ github.run_id }} -githubToken ${{ github.token }} -githubRepo "${{ github.repository }}" -jobNameRegex "^runlog compatibility with (.*?)$" -namedKey="runlog" -outputFile=output.json @@ -812,7 +806,6 @@ jobs: asciitable --firstColumn "EVM Implementation" --secondColumn Result --jsonfile input.json --outputFile output.txt --section "ocr" --namedKey "ocr" asciitable --firstColumn "EVM Implementation" --secondColumn Result --jsonfile input.json --outputFile output.txt --section "ocr2" --namedKey "ocr2" asciitable --firstColumn "EVM Implementation" --secondColumn Result --jsonfile input.json --outputFile output.txt --section "vrf" --namedKey "vrf" - asciitable --firstColumn "EVM Implementation" --secondColumn Result --jsonfile input.json --outputFile output.txt --section "vrfv2" --namedKey "vrfv2" asciitable --firstColumn "EVM Implementation" --secondColumn Result --jsonfile input.json --outputFile output.txt --section "vrfv2plus" --namedKey "vrfv2plus" asciitable --firstColumn "EVM Implementation" --secondColumn Result --jsonfile input.json --outputFile output.txt --section "flux" --namedKey "flux" asciitable --firstColumn "EVM Implementation" --secondColumn Result --jsonfile input.json --outputFile output.txt --section "cron" --namedKey "cron" @@ -849,7 +842,6 @@ jobs: - keeper - log_poller - vrf - - vrfv2 - vrfv2plus steps: - name: Checkout the repo diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f0fd9a70042..583699c29e7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -43,9 +43,9 @@ jobs: build-mode: none steps: - name: Enable S3 Cache for Self-Hosted Runners - # these env vars are set (and exposed) when it is a self-hosted runner with extras=s3-cache - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/cre-local-env-tests.yaml b/.github/workflows/cre-local-env-tests.yaml index 95f2553cd17..eff4e33f8ae 100644 --- a/.github/workflows/cre-local-env-tests.yaml +++ b/.github/workflows/cre-local-env-tests.yaml @@ -77,9 +77,9 @@ jobs: ref: ${{ github.event_name == 'pull_request' && github.sha || inputs.chainlink_version }} - name: Enable S3 Cache for Self-Hosted Runners - # these env vars are set (and exposed) when it is a self-hosted runner with extras=s3-cache - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Set up Go id: setup-go @@ -244,7 +244,6 @@ jobs: go run . env swap nodes - name: Execute example PoR workflow - if: false # TODO: Migrate example test to V2 and run here (DX-3233) shell: bash working-directory: core/scripts/cre/environment run: | diff --git a/.github/workflows/cre-regression-system-tests.yaml b/.github/workflows/cre-regression-system-tests.yaml index 02e255d34aa..a4ec65b103d 100644 --- a/.github/workflows/cre-regression-system-tests.yaml +++ b/.github/workflows/cre-regression-system-tests.yaml @@ -49,9 +49,9 @@ jobs: persist-credentials: false - name: Enable S3 Cache for Self-Hosted Runners - # these env vars are set (and exposed) when it is a self-hosted runner with extras=s3-cache - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Set up Go id: setup-go @@ -110,9 +110,9 @@ jobs: steps: - name: Enable S3 Cache for Self-Hosted Runners - # these env vars are set (and exposed) when it is a self-hosted runner with extras=s3-cache - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/cre-soak-memory-leak.yml b/.github/workflows/cre-soak-memory-leak.yml index 7e2b7c97f2a..3121aa54c9a 100644 --- a/.github/workflows/cre-soak-memory-leak.yml +++ b/.github/workflows/cre-soak-memory-leak.yml @@ -43,8 +43,9 @@ jobs: steps: - name: Enable S3 Cache for Self-Hosted Runners - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/cre-system-tests.yaml b/.github/workflows/cre-system-tests.yaml index 16225239a3f..ba27f356917 100644 --- a/.github/workflows/cre-system-tests.yaml +++ b/.github/workflows/cre-system-tests.yaml @@ -49,9 +49,9 @@ jobs: persist-credentials: false - name: Enable S3 Cache for Self-Hosted Runners - # these env vars are set (and exposed) when it is a self-hosted runner with extras=s3-cache - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Set up Go id: setup-go @@ -157,9 +157,9 @@ jobs: steps: - name: Enable S3 Cache for Self-Hosted Runners - # these env vars are set (and exposed) when it is a self-hosted runner with extras=s3-cache - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3b9ab2341ac..73385d5ab9e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -34,6 +34,7 @@ jobs: }} runner-arm64: ${{ steps.runner-labels.outputs.runner-arm64 }} runner-amd64: ${{ steps.runner-labels.outputs.runner-amd64 }} + checked-out-sha: ${{ steps.checkout-sha.outputs.checked-out-sha }} version-tag: ${{ steps.version-info.outputs.version-tag }} steps: - name: Get PR Labels @@ -65,6 +66,12 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + ref: ${{ inputs.git-ref || github.sha }} + + - name: Resolve checked out SHA + id: checkout-sha + shell: bash + run: echo "checked-out-sha=$(git rev-parse HEAD)" | tee -a "$GITHUB_OUTPUT" - name: Version Info id: version-info @@ -88,10 +95,10 @@ jobs: docker-build-context: . docker-build-args: | CHAINLINK_USER=chainlink - COMMIT_SHA=${{ github.sha }} + COMMIT_SHA=${{ needs.init.outputs.checked-out-sha }} VERSION_TAG=${{ needs.init.outputs.version-tag }} docker-manifest-sign: true - git-sha: ${{ inputs.git-ref || github.sha }} + git-sha: ${{ needs.init.outputs.checked-out-sha }} github-event-name: ${{ github.event_name }} github-ref-name: ${{ github.ref_name }} github-ref-type: ${{ github.ref_type}} @@ -120,12 +127,12 @@ jobs: docker-build-context: . docker-build-args: | CHAINLINK_USER=chainlink - COMMIT_SHA=${{ github.sha }} + COMMIT_SHA=${{ needs.init.outputs.checked-out-sha }} VERSION_TAG=${{ needs.init.outputs.version-tag }} CL_INSTALL_PRIVATE_PLUGINS=true docker-manifest-sign: true docker-tag-custom-suffix: "-plugins" - git-sha: ${{ inputs.git-ref || github.sha }} + git-sha: ${{ needs.init.outputs.checked-out-sha }} github-event-name: ${{ github.event_name }} github-ref-name: ${{ github.ref_name }} github-ref-type: ${{ github.ref_type}} @@ -154,13 +161,13 @@ jobs: docker-build-context: . docker-build-args: | CHAINLINK_USER=chainlink - COMMIT_SHA=${{ github.sha }} + COMMIT_SHA=${{ needs.init.outputs.checked-out-sha }} VERSION_TAG=${{ needs.init.outputs.version-tag }} CL_INSTALL_PRIVATE_PLUGINS=true CL_INSTALL_TESTING_PLUGINS=true docker-manifest-sign: true docker-tag-custom-suffix: "-plugins-testing" - git-sha: ${{ inputs.git-ref || github.sha }} + git-sha: ${{ needs.init.outputs.checked-out-sha }} github-event-name: ${{ github.event_name }} github-ref-name: ${{ github.ref_name }} github-ref-type: ${{ github.ref_type}} @@ -189,13 +196,13 @@ jobs: docker-build-context: . docker-build-args: | CHAINLINK_USER=chainlink - COMMIT_SHA=${{ github.sha }} + COMMIT_SHA=${{ needs.init.outputs.checked-out-sha }} VERSION_TAG=${{ needs.init.outputs.version-tag }} CL_INSTALL_PRIVATE_PLUGINS=true CL_CHAIN_DEFAULTS=/ccip-config CL_SOLANA_CMD= docker-manifest-sign: true - git-sha: ${{ inputs.git-ref || github.sha }} + git-sha: ${{ needs.init.outputs.checked-out-sha }} github-event-name: ${{ github.event_name }} github-ref-name: ${{ github.ref_name }} github-ref-type: ${{ github.ref_type}} @@ -224,13 +231,13 @@ jobs: docker-build-context: . docker-build-args: | CHAINLINK_USER=chainlink - COMMIT_SHA=${{ github.sha }} + COMMIT_SHA=${{ needs.init.outputs.checked-out-sha }} VERSION_TAG=${{ needs.init.outputs.version-tag }} CL_INSTALL_PRIVATE_PLUGINS=true CL_CHAIN_DEFAULTS=/ccip-config docker-manifest-sign: true docker-tag-custom-suffix: "-plugins" - git-sha: ${{ inputs.git-ref || github.sha }} + git-sha: ${{ needs.init.outputs.checked-out-sha }} github-event-name: ${{ github.event_name }} github-ref-name: ${{ github.ref_name }} github-ref-type: ${{ github.ref_type}} diff --git a/.github/workflows/go-mod-cache.yml b/.github/workflows/go-mod-cache.yml index 42c36186a84..22e0d6c095d 100644 --- a/.github/workflows/go-mod-cache.yml +++ b/.github/workflows/go-mod-cache.yml @@ -53,9 +53,9 @@ jobs: pull-requests: read steps: - name: Enable S3 Cache for Self-Hosted Runners - # these env vars are set (and exposed) when it is a self-hosted runner with extras=s3-cache - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Checkout the repo uses: actions/checkout@v4 diff --git a/.github/workflows/integration-in-memory-tests.yml b/.github/workflows/integration-in-memory-tests.yml index 3a3ea598d3d..11f01e639ce 100644 --- a/.github/workflows/integration-in-memory-tests.yml +++ b/.github/workflows/integration-in-memory-tests.yml @@ -30,12 +30,14 @@ jobs: deployment: false name: Check Paths That Require Tests To Run runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read # We don't directly merge dependabot PRs, so let's not waste the resources if: github.actor != 'dependabot[bot]' outputs: - github_ci_changes: ${{ steps.ignore-filter.outputs.changes || steps.changes.outputs.github_ci_changes }} - core_changes: ${{ steps.ignore-filter.outputs.changes || steps.changes.outputs.core_changes }} - ccip_changes: ${{ steps.ignore-filter.outputs.changes || steps.changes.outputs.ccip_changes }} + run-tests: ${{ steps.advanced-triggers.outputs.run-tests }} should_use_self_hosted_runner: ${{ steps.label-runs-on-opt-out.outputs.check-label-found == 'false' }} steps: - name: Checkout the repo @@ -45,29 +47,43 @@ jobs: repository: smartcontractkit/chainlink ref: ${{ inputs.cl_ref }} - - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 - id: changes + - name: Advanced Triggers + uses: smartcontractkit/.github/actions/advanced-triggers@advanced-triggers/v1 + id: advanced-triggers with: - filters: | - github_ci_changes: - - '.github/workflows/integration-in-memory-tests.yml' - - '.github/integration-in-memory-tests.yml' - core_changes: - - '**/*.go' - - '**/*go.sum' - - '**/*go.mod' - - '**/*Dockerfile' - - 'core/**/migrations/*.sql' - - 'core/**/config/**/*.toml' - - 'integration-tests/**/*.toml' - ccip_changes: - - '**/*ccip*' - - '**/*ccip*/**' - - - name: Ignore Filter On Workflow Dispatch - if: ${{ github.event_name == 'workflow_dispatch' }} - id: ignore-filter - run: echo "changes=true" >> $GITHUB_OUTPUT + file-sets: | + go-files: + - "**/*.go" + - "**/go.mod" + - "**/go.sum" + core-files: + - "core/**" + deployment-files: + - "deployment/**" + workflow-files: + - ".github/workflows/integration-in-memory-tests.yml" + - ".github/integration-in-memory-tests.yml" + - ".github/actions/**/*.y*ml" + core-test-files: + - "testdata/**" + - "core/**/testdata/**" + - "core/**/*_test.go" + deployment-test-files: + - "deployment/**/*_test.go" + - "deployment/**/testdata/**" + to-ignore-files: + - "system-tests/**" + - "devenv/**" + - "core/scripts/cre/**" + triggers: | + run-tests: + exclusion-sets: [ core-test-files, deployment-test-files, to-ignore-files ] + inclusion-sets: [ go-files, core-files, deployment-files, workflow-files ] + paths: + - "integration-tests/**" + - "**/*Dockerfile" + - '**/ccip/**' + - '**/*ccip*/**' - name: Get PR Labels (runs-on-opt-out) id: label-runs-on-opt-out @@ -75,7 +91,7 @@ jobs: with: check-label: "runs-on-opt-out" - run-ccip-integration-in-memory--tests-for-pr: + run-ccip-integration-in-memory-tests-for-pr: name: Run CCIP integration In Memory Tests For PR permissions: actions: read @@ -84,7 +100,7 @@ jobs: id-token: write contents: read needs: changes - if: github.event_name == 'pull_request' && ( needs.changes.outputs.ccip_changes == 'true' || needs.changes.outputs.core_changes == 'true' || needs.changes.outputs.github_ci_changes == 'true') + if: github.event_name == 'pull_request' && needs.changes.outputs.run-tests == 'true' uses: smartcontractkit/.github/.github/workflows/run-e2e-tests.yml@fed09778ce127da5d37f902d8bee01387856550a # 2026-03-12 with: workflow_name: Run CCIP Integration Tests For PR @@ -116,7 +132,7 @@ jobs: id-token: write contents: read needs: changes - if: github.event_name == 'merge_group' && ( needs.changes.outputs.ccip_changes == 'true' || needs.changes.outputs.core_changes == 'true' || needs.changes.outputs.github_ci_changes == 'true') + if: github.event_name == 'merge_group' && needs.changes.outputs.run-tests == 'true' uses: smartcontractkit/.github/.github/workflows/run-e2e-tests.yml@fed09778ce127da5d37f902d8bee01387856550a # 2026-03-12 with: workflow_name: Run CCIP Integration Tests For Merge Queue @@ -148,12 +164,12 @@ jobs: runs-on: ubuntu-latest needs: [ - run-ccip-integration-in-memory--tests-for-pr, + run-ccip-integration-in-memory-tests-for-pr, run-ccip-integration-in-memory-tests-for-merge-queue, ] steps: - name: Fail the job if ccip tests in PR not successful - if: always() && needs.run-ccip-integration-in-memory--tests-for-pr.result == 'failure' + if: always() && needs.run-ccip-integration-in-memory-tests-for-pr.result == 'failure' run: exit 1 - name: Fail the job if ccip tests in merge queue not successful diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 54cc3ba24fc..6f6c8fb8010 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -243,9 +243,9 @@ jobs: }} steps: - name: Enable S3 Cache for Self-Hosted Runners - # these env vars are set (and exposed) when it is a self-hosted runner with extras=s3-cache - if: ${{ env.RUNS_ON_INSTANCE_ID != '' && env.ACTIONS_CACHE_URL != '' }} - uses: runs-on/action@66d4449b717b5462159659523d1241051ff470b9 # v1 + uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0 + with: + metrics: cpu,network,memory,disk - name: Check if image exists in ECR id: check-image-exists diff --git a/.github/workflows/on-demand-vrfv2-smoke-tests.yml b/.github/workflows/on-demand-vrfv2-smoke-tests.yml deleted file mode 100644 index c75b28a33ca..00000000000 --- a/.github/workflows/on-demand-vrfv2-smoke-tests.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: On Demand VRFV2 Smoke Tests -on: - workflow_dispatch: - inputs: - test_suite: - description: "Test Suite to run" - required: true - type: choice - default: "All Tests" - options: - - "All Tests" - - "Selected Tests" - test_list_regex: - description: "Regex for 'Selected Tests' to run" - required: false - default: "TestVRFv2Basic/(Request_Randomness|Direct_Funding)|TestVRFV2WithBHS" - test_config_override_path: - description: Path to a test config file used to override the default test config - required: false - type: string - test_secrets_override_key: - description: "Key to run tests with custom test secrets" - required: false - type: string - chainlink_version: - description: Chainlink image version to use - default: develop - required: false - type: string - notify_user_id_on_failure: - description: "Enter Slack user ID to notify on test failure" - required: false - type: string - -jobs: - set-tests-to-run: - name: Set tests to run - runs-on: ubuntu-latest - outputs: - test_list: ${{ steps.set-tests.outputs.test_list }} - steps: - - name: Generate Test List JSON - id: set-tests - env: - GH_INPUTS_TEST_SUITE: ${{ inputs.test_suite }} - GH_INPUTS_TEST_LIST_REGEX: ${{ inputs.test_list_regex }} - GH_INPUTS_TEST_CONFIG_OVERRIDE_PATH: ${{ inputs.test_config_override_path }} - run: | - if [[ "$GH_INPUTS_TEST_SUITE" == "All Tests" ]]; then - TEST_CMD="cd integration-tests/smoke && go test vrfv2_test.go -test.parallel=1 -timeout 3h -count=1 -json -v" - else - TEST_CMD='cd integration-tests/smoke && go test -test.run "$GH_INPUTS_TEST_LIST_REGEX" -test.parallel=1 -timeout 2h -count=1 -json -v' - fi - TEST_CONFIG_OVERRIDE_PATH=$GH_INPUTS_TEST_CONFIG_OVERRIDE_PATH - - TEST_LIST=$(jq -n -c \ - --arg test_cmd "$TEST_CMD" \ - --arg test_config_override_path "$TEST_CONFIG_OVERRIDE_PATH" \ - '{ - "tests": [ - { - "id": "TestVRFv2_Smoke", - "path": "integration-tests/smoke/vrfv2_test.go", - "runs_on": "ubuntu-latest", - "test_env_type": "docker", - "test_cmd": $test_cmd, - "test_config_override_path": $test_config_override_path - } - ] - }') - - echo "test_list=$TEST_LIST" >> $GITHUB_OUTPUT - - run-e2e-tests-workflow: - name: Run E2E Tests - needs: set-tests-to-run - uses: smartcontractkit/.github/.github/workflows/run-e2e-tests.yml@fed09778ce127da5d37f902d8bee01387856550a - with: - custom_test_list_json: ${{ needs.set-tests-to-run.outputs.test_list }} - chainlink_version: ${{ inputs.chainlink_version }} - slack_notification_after_tests: always - slack_notification_after_tests_name: "VRF V2 Smoke Tests with test config: ${{ inputs.test_config_override_path || 'default' }}" - slack_notification_after_tests_notify_user_id_on_failure: ${{ inputs.notify_user_id_on_failure }} - test_secrets_override_key: ${{ inputs.test_secrets_override_key }} - secrets: - QA_AWS_REGION: ${{ secrets.QA_AWS_REGION }} - QA_AWS_ROLE_TO_ASSUME: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} - QA_AWS_ACCOUNT_NUMBER: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} - PROD_AWS_ACCOUNT_NUMBER: ${{ secrets.AWS_ACCOUNT_ID_PROD }} - QA_PYROSCOPE_INSTANCE: ${{ secrets.QA_PYROSCOPE_INSTANCE }} - QA_PYROSCOPE_KEY: ${{ secrets.QA_PYROSCOPE_KEY }} - GRAFANA_INTERNAL_TENANT_ID: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - GRAFANA_INTERNAL_BASIC_AUTH: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} - GRAFANA_INTERNAL_HOST: ${{ secrets.GRAFANA_INTERNAL_HOST }} - GRAFANA_INTERNAL_URL_SHORTENER_TOKEN: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - LOKI_TENANT_ID: ${{ secrets.LOKI_TENANT_ID }} - LOKI_URL: ${{ secrets.LOKI_URL }} - LOKI_BASIC_AUTH: ${{ secrets.LOKI_BASIC_AUTH }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AWS_REGION: ${{ secrets.QA_AWS_REGION }} - AWS_OIDC_IAM_ROLE_VALIDATION_PROD_ARN: ${{ secrets.AWS_OIDC_IAM_ROLE_VALIDATION_PROD_ARN }} - AWS_API_GW_HOST_GRAFANA: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} - TEST_SECRETS_OVERRIDE_BASE64: ${{ secrets[inputs.test_secrets_override_key] }} - SLACK_BOT_TOKEN: ${{ secrets.QA_SLACK_API_KEY }} - SLACK_NOTIFICATION_AFTER_TESTS_CHANNEL_ID: ${{ secrets.QA_VRF_SLACK_CHANNEL }} - MAIN_DNS_ZONE_PUBLIC_SDLC: ${{ secrets.MAIN_DNS_ZONE_PUBLIC_SDLC }} - AWS_K8S_CLUSTER_NAME_SDLC: ${{ secrets.AWS_K8S_CLUSTER_NAME_SDLC }} diff --git a/.github/workflows/system-tests-nightly.yml b/.github/workflows/system-tests-nightly.yml index ec920fce7e7..39b5f63dc9e 100644 --- a/.github/workflows/system-tests-nightly.yml +++ b/.github/workflows/system-tests-nightly.yml @@ -4,7 +4,6 @@ on: schedule: - cron: "0 6 * * *" # Run daily at 6 AM workflow_dispatch: - pull_request: defaults: run: @@ -235,6 +234,30 @@ jobs: runner: "ubuntu24.04-16cores-64GB" tests_dir: "vrfv2plus" logs_archive_name: "vrfv2plus-batch-fulfillment" + - display_name: "Test VRFv2 (coordinator v2) Smoke" + testcmd: "go test -v -timeout 30m -run TestVRFv2Basic" + envcmd: "cl u env-vrfv2.toml,products/vrfv2/basic.toml" + runner: "ubuntu24.04-16cores-64GB" + tests_dir: "vrfv2" + logs_archive_name: "vrfv2-smoke" + - display_name: "Test VRFv2 Multiple Sending Keys" + testcmd: "go test -v -timeout 30m -run TestVRFv2MultipleSendingKeys" + envcmd: "cl u env-vrfv2.toml,products/vrfv2/two_keys.toml" + runner: "ubuntu24.04-16cores-64GB" + tests_dir: "vrfv2" + logs_archive_name: "vrfv2-multiple-sending-keys" + - display_name: "Test VRFv2 With BHS" + testcmd: "go test -v -timeout 30m -run TestVRFV2WithBHS" + envcmd: "cl u env-vrfv2-bhs.toml,products/vrfv2/bhs.toml" + runner: "ubuntu24.04-16cores-64GB" + tests_dir: "vrfv2" + logs_archive_name: "vrfv2-with-bhs" + - display_name: "Test VRFv2 Batch Fulfillment" + testcmd: "go test -v -timeout 30m -run TestVRFv2BatchFulfillmentEnabledDisabled" + envcmd: "cl u env-vrfv2.toml,products/vrfv2/basic.toml" + runner: "ubuntu24.04-16cores-64GB" + tests_dir: "vrfv2" + logs_archive_name: "vrfv2-batch-fulfillment" - display_name: "Test LogPoller Fixed Depth" testcmd: "go test -v -timeout 30m -run TestLogPoller" envcmd: "cl u env.toml,tests/logpoller/fixed_depth.toml" diff --git a/CHANGELOG.md b/CHANGELOG.md index d04589251d7..afbaada5880 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog Chainlink Core +## 2.40.0 + +### Minor Changes + +- [#21566](https://github.com/smartcontractkit/chainlink/pull/21566) [`540693c`](https://github.com/smartcontractkit/chainlink/commit/540693cc71f44ccfcb89f36c959890fd5cf324fa) - Minor bump to start next version + +### Patch Changes + +- [#21643](https://github.com/smartcontractkit/chainlink/pull/21643) [`e034db3`](https://github.com/smartcontractkit/chainlink/commit/e034db36cc3a02a79aec1a3b44ccc71ddc2f21ed) - #added Add ListPendingJobProposals and ApproveJobProposalByID to the GQL SDK Client + +- [#21550](https://github.com/smartcontractkit/chainlink/pull/21550) [`6ed0e26`](https://github.com/smartcontractkit/chainlink/commit/6ed0e268f955a20b38a0c052efd2acac07c798c2) - #changed Bumps the chainlink-ccip reference and replaces all references to `latest` with version-locked imports + +- [#21604](https://github.com/smartcontractkit/chainlink/pull/21604) [`4561cb7`](https://github.com/smartcontractkit/chainlink/commit/4561cb741754d00b2910599952ef5e1a5abde45c) - #added Emit gas-related metrics through Beholder + ## 2.39.0 ### Minor Changes diff --git a/GNUmakefile b/GNUmakefile index 255be93a03a..02f920aa34a 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -115,6 +115,7 @@ install-plugins-local: ## Build & install local plugins install-plugins: install-plugins-local install-plugins-public ## Build and install local and public plugins via loopinstall .PHONY: docker ## Build the chainlink docker image +docker: DOCKER_TAG=develop docker: @if ([ "$(CL_INSTALL_PRIVATE_PLUGINS)" = "true" ] || [ "$(CL_INSTALL_TESTING_PLUGINS)" = "true" ]) && [ -z "$(GITHUB_TOKEN)" ]; then \ echo "Error: GITHUB_TOKEN environment variable is required when CL_INSTALL_PRIVATE_PLUGINS=true or CL_INSTALL_TESTING_PLUGINS=true"; \ @@ -124,19 +125,22 @@ docker: docker buildx build \ --build-arg COMMIT_SHA=$(COMMIT_SHA) \ --build-arg VERSION_TAG=$(VERSION_TAG) \ + --build-arg CL_AUTO_DOCKER_TAG=$(DOCKER_TAG) \ --build-arg CL_INSTALL_PRIVATE_PLUGINS=$(CL_INSTALL_PRIVATE_PLUGINS) \ --build-arg CL_IS_PROD_BUILD=$(CL_IS_PROD_BUILD) \ $(PRIVATE_PLUGIN_ARGS) \ -f core/chainlink.Dockerfile . \ - -t chainlink:develop \ + -t chainlink:$(DOCKER_TAG) \ --load .PHONY: docker-ccip ## Build the chainlink docker image +docker-ccip: DOCKER_TAG=latest docker-ccip: docker buildx build \ --build-arg COMMIT_SHA=$(COMMIT_SHA) \ --build-arg VERSION_TAG=$(VERSION_TAG) \ - -f core/chainlink.Dockerfile . -t chainlink-ccip:latest + --build-arg CL_AUTO_DOCKER_TAG=$(DOCKER_TAG) \ + -f core/chainlink.Dockerfile . -t chainlink-ccip:$(DOCKER_TAG) docker buildx build \ --build-arg COMMIT_SHA=$(COMMIT_SHA) \ @@ -146,6 +150,7 @@ docker-ccip: # Define a comma variable for use in $(eval) (needed for the PRIVATE_PLUGIN_ARGS) comma := , .PHONY: docker-plugins ## Build the EXPERIMENTAL chainlink-plugins docker image +docker-plugins: DOCKER_TAG=latest docker-plugins: @if ([ "$(CL_INSTALL_PRIVATE_PLUGINS)" = "true" ] || [ "$(CL_INSTALL_TESTING_PLUGINS)" = "true" ]) && [ -z "$(GITHUB_TOKEN)" ]; then \ echo "Error: GITHUB_TOKEN environment variable is required when CL_INSTALL_PRIVATE_PLUGINS=true or CL_INSTALL_TESTING_PLUGINS=true"; \ @@ -155,11 +160,12 @@ docker-plugins: docker buildx build \ --build-arg COMMIT_SHA=$(COMMIT_SHA) \ --build-arg VERSION_TAG=$(VERSION_TAG) \ + --build-arg CL_AUTO_DOCKER_TAG=$(DOCKER_TAG) \ --build-arg CL_INSTALL_TESTING_PLUGINS=$(CL_INSTALL_TESTING_PLUGINS) \ --build-arg CL_INSTALL_PRIVATE_PLUGINS=$(CL_INSTALL_PRIVATE_PLUGINS) \ $(PRIVATE_PLUGIN_ARGS) \ -f plugins/chainlink.Dockerfile . \ - -t chainlink-plugins:latest + -t chainlink-plugins:$(DOCKER_TAG) .PHONY: operator-ui operator-ui: ## Fetch the frontend diff --git a/core/capabilities/remote/executable/request/server_request.go b/core/capabilities/remote/executable/request/server_request.go index c34466b13fe..b759021f417 100644 --- a/core/capabilities/remote/executable/request/server_request.go +++ b/core/capabilities/remote/executable/request/server_request.go @@ -208,6 +208,11 @@ func (e *ServerRequest) Expired() bool { return time.Since(e.createdTime) > e.requestTimeout } +func (e *ServerRequest) Evictable(minRetention time.Duration) bool { + age := time.Since(e.createdTime) + return age > e.requestTimeout && age > minRetention +} + func (e *ServerRequest) Cancel(ctx context.Context, err types.Error, msg string) error { e.mux.Lock() defer e.mux.Unlock() diff --git a/core/capabilities/remote/executable/request/server_request_test.go b/core/capabilities/remote/executable/request/server_request_test.go index aad90136892..19b9cf5b549 100644 --- a/core/capabilities/remote/executable/request/server_request_test.go +++ b/core/capabilities/remote/executable/request/server_request_test.go @@ -338,6 +338,42 @@ func Test_ServerRequest_MessageValidation(t *testing.T) { }) } +func Test_ServerRequest_Evictable(t *testing.T) { + lggr := logger.Test(t) + capability := TestCapability{} + capabilityPeerID := NewP2PPeerID(t) + workflowPeer := NewP2PPeerID(t) + + callingDon := commoncap.DON{ + Members: []p2ptypes.PeerID{workflowPeer}, + ID: 1, + F: 0, + } + + newRequest := func(requestTimeout time.Duration) *request.ServerRequest { + req, err := request.NewServerRequest(capability, types.MethodExecute, "capabilityID", 2, + capabilityPeerID, callingDon, "requestMessageID", &testDispatcher{}, requestTimeout, "", lggr) + require.NoError(t, err) + return req + } + + t.Run("expired but below minimum retention", func(t *testing.T) { + req := newRequest(20 * time.Millisecond) + require.Eventually(t, func() bool { return req.Expired() }, time.Second, 10*time.Millisecond) + assert.False(t, req.Evictable(200*time.Millisecond)) + }) + + t.Run("expired and retained past minimum retention", func(t *testing.T) { + req := newRequest(20 * time.Millisecond) + require.Eventually(t, func() bool { return req.Evictable(10 * time.Millisecond) }, time.Second, 10*time.Millisecond) + }) + + t.Run("minimum retention elapsed but request timeout still active", func(t *testing.T) { + req := newRequest(200 * time.Millisecond) + require.Never(t, func() bool { return req.Evictable(10 * time.Millisecond) }, 100*time.Millisecond, 10*time.Millisecond) + }) +} + type serverRequest interface { OnMessage(ctx context.Context, msg *types.MessageBody) error } diff --git a/core/capabilities/remote/executable/server.go b/core/capabilities/remote/executable/server.go index b53b5d1dbf5..a2406997114 100644 --- a/core/capabilities/remote/executable/server.go +++ b/core/capabilities/remote/executable/server.go @@ -229,6 +229,8 @@ func (r *server) expireRequests() { if err != nil { r.lggr.Errorw("failed to cancel request", "request", executeReq, "err", err) } + } + if executeReq.request.Evictable(commoncap.DefaultExecutableRequestTimeout) { delete(r.requestIDToRequest, requestID) delete(r.messageIDToRequestIDsCount, executeReq.messageID) } diff --git a/core/capabilities/remote/executable/server_test.go b/core/capabilities/remote/executable/server_test.go index b5192127c0e..0b89c722f23 100644 --- a/core/capabilities/remote/executable/server_test.go +++ b/core/capabilities/remote/executable/server_test.go @@ -1000,3 +1000,109 @@ func Test_Server_Execute_WithConcurrentSetConfig(t *testing.T) { expectedResponses := numWorkflowPeers * numExecuteCalls require.Equal(t, expectedResponses, successCount) } + +func Test_Server_DuplicateRequestRemainsDedupedPastRequestTimeout(t *testing.T) { + ctx := testutils.Context(t) + lggr := logger.Test(t) + + serverPeerID := NewP2PPeerID(t) + senderPeerID := NewP2PPeerID(t) + + dispatcher := &noopDispatcher{} + server := executable.NewServer("cap_id@1.0.0", "", serverPeerID, dispatcher, lggr) + + cfg := &commoncap.RemoteExecutableConfig{ + RequestTimeout: 20 * time.Millisecond, + ServerMaxParallelRequests: 1, + } + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id@1.0.0", + CapabilityType: commoncap.CapabilityTypeTarget, + } + localDON := commoncap.DON{ + ID: 1, + Members: []p2ptypes.PeerID{serverPeerID}, + F: 0, + } + workflowDONs := map[uint32]commoncap.DON{ + 2: { + ID: 2, + Members: []p2ptypes.PeerID{senderPeerID}, + F: 0, + }, + } + + require.NoError(t, server.SetConfig(cfg, TestCapability{}, capInfo, localDON, workflowDONs, nil)) + require.NoError(t, server.Start(ctx)) + defer func() { + require.NoError(t, server.Close()) + }() + + inputs, err := values.NewMap(map[string]any{"executeValue1": "aValue1"}) + require.NoError(t, err) + rawRequest, err := pb.MarshalCapabilityRequest(commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowExecutionID: "exec-1", + }, + Inputs: inputs, + }) + require.NoError(t, err) + + msg := &remotetypes.MessageBody{ + CapabilityId: capInfo.ID, + CapabilityDonId: localDON.ID, + CallerDonId: 2, + Method: remotetypes.MethodExecute, + Payload: rawRequest, + MessageId: []byte(remotetypes.MethodExecute + ":exec-1"), + Sender: senderPeerID[:], + Receiver: serverPeerID[:], + } + + server.Receive(ctx, msg) + require.Eventually(t, func() bool { return dispatcher.Len() == 1 }, time.Second, 10*time.Millisecond) + + time.Sleep(2 * cfg.RequestTimeout) + server.Receive(ctx, msg) + + require.Never(t, func() bool { return dispatcher.Len() > 1 }, 100*time.Millisecond, 10*time.Millisecond) +} + +type noopDispatcher struct { + services.StateMachine + mu sync.Mutex + sent []*remotetypes.MessageBody +} + +func (n *noopDispatcher) Name() string { return "noopDispatcher" } + +func (n *noopDispatcher) Start(context.Context) error { return nil } + +func (n *noopDispatcher) Close() error { return nil } + +func (n *noopDispatcher) Ready() error { return nil } + +func (n *noopDispatcher) HealthReport() map[string]error { return nil } + +func (n *noopDispatcher) SetReceiver(string, uint32, remotetypes.Receiver) error { return nil } + +func (n *noopDispatcher) RemoveReceiver(string, uint32) {} + +func (n *noopDispatcher) SetReceiverForMethod(string, uint32, string, remotetypes.Receiver) error { + return nil +} + +func (n *noopDispatcher) RemoveReceiverForMethod(string, uint32, string) {} + +func (n *noopDispatcher) Send(peerID p2ptypes.PeerID, msgBody *remotetypes.MessageBody) error { + n.mu.Lock() + defer n.mu.Unlock() + n.sent = append(n.sent, msgBody) + return nil +} + +func (n *noopDispatcher) Len() int { + n.mu.Lock() + defer n.mu.Unlock() + return len(n.sent) +} diff --git a/core/capabilities/vault/capability.go b/core/capabilities/vault/capability.go index 55375a08673..74430246c6b 100644 --- a/core/capabilities/vault/capability.go +++ b/core/capabilities/vault/capability.go @@ -3,10 +3,8 @@ package vault import ( "context" "encoding/hex" - "encoding/json" "errors" "fmt" - "strconv" "strings" "time" @@ -17,7 +15,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" vaultcommon "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/vault" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/requests" - jsonrpc "github.com/smartcontractkit/chainlink-common/pkg/jsonrpc2" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" @@ -32,7 +29,6 @@ type Capability struct { clock clockwork.Clock expiresAfter time.Duration handler *requests.Handler[*vaulttypes.Request, *vaulttypes.Response] - requestAuthorizer RequestAuthorizer capabilitiesRegistry core.CapabilitiesRegistry publicKey *LazyPublicKey *RequestValidator @@ -167,24 +163,6 @@ func (s *Capability) CreateSecrets(ctx context.Context, request *vaultcommon.Cre s.lggr.Debugf("RequestId: [%s] failed validation checks: %s", request.RequestId, err.Error()) return nil, err } - authorized, owner, err := s.authorizeCreateSecrets(ctx, *request) //nolint:govet // The mutex isn't used - if !authorized || err != nil { - s.lggr.Debugf("Request Id[%s] not authorized for owner: %s", request.RequestId, owner) - return nil, errors.New("request ID: " + request.RequestId + " not authorized: " + err.Error()) - } - if !strings.HasPrefix(request.RequestId, owner) { - // Gateway should ensure it prefixes request ids with the owner, to ensure request uniqueness - s.lggr.Debugf("Request ID: [%s] must start with owner address: [%s]", request.RequestId, owner) - return nil, errors.New("request ID: " + request.RequestId + " must start with owner address: " + owner) - } - for idx, req := range request.EncryptedSecrets { - // Ensure that users cannot access secrets belonging to other owners - if req.Id.Owner != owner { - s.lggr.Debugf("Secret ID owner: [%s] does not match authorized owner: [%s]", req.Id.Owner, owner) - return nil, errors.New("secret ID owner: " + req.Id.Owner + " does not match authorized owner: " + owner + " at index " + strconv.Itoa(idx)) - } - } - s.lggr.Debugf("Processing authorized and normalized request [%s]", request.String()) return s.handleRequest(ctx, request.RequestId, request) } @@ -195,24 +173,6 @@ func (s *Capability) UpdateSecrets(ctx context.Context, request *vaultcommon.Upd s.lggr.Debugf("RequestId: [%s] failed validation checks: %s", request.RequestId, err.Error()) return nil, err } - authorized, owner, err := s.authorizeUpdateSecrets(ctx, *request) //nolint:govet // The mutex isn't used - if !authorized || err != nil { - s.lggr.Debugf("Request Id[%s] not authorized for owner: %s", request.RequestId, owner) - return nil, errors.New("request ID: " + request.RequestId + " not authorized: " + err.Error()) - } - if !strings.HasPrefix(request.RequestId, owner) { - // Gateway should ensure it prefixes request ids with the owner, to ensure request uniqueness - s.lggr.Debugf("Request ID: [%s] must start with owner address: [%s]", request.RequestId, owner) - return nil, errors.New("request ID: " + request.RequestId + " must start with owner address: " + owner) - } - for idx, req := range request.EncryptedSecrets { - // Ensure that users cannot access secrets belonging to other owners - if req.Id.Owner != owner { - s.lggr.Debugf("Secret ID owner: [%s] does not match authorized owner: [%s]", req.Id.Owner, owner) - return nil, errors.New("secret ID owner: " + req.Id.Owner + " does not match authorized owner: " + owner + " at index " + strconv.Itoa(idx)) - } - } - s.lggr.Debugf("Processing authorized and normalized request [%s]", request.String()) return s.handleRequest(ctx, request.RequestId, request) } @@ -223,25 +183,6 @@ func (s *Capability) DeleteSecrets(ctx context.Context, request *vaultcommon.Del s.lggr.Debugf("Request: [%s] failed validation checks: %s", request.String(), err.Error()) return nil, err } - - authorized, owner, err := s.authorizeDeleteSecrets(ctx, *request) //nolint:govet // The mutex isn't used - if !authorized || err != nil { - s.lggr.Debugf("Request Id[%s] not authorized for owner: %s", request.RequestId, owner) - return nil, errors.New("request ID: " + request.RequestId + " not authorized: " + err.Error()) - } - if !strings.HasPrefix(request.RequestId, owner) { - // Gateway should ensure it prefixes request ids with the owner, to ensure request uniqueness - s.lggr.Debugf("Request ID: [%s] must start with owner address: [%s]", request.RequestId, owner) - return nil, errors.New("request ID: " + request.RequestId + " must start with owner address: " + owner) - } - for idx, req := range request.Ids { - // Ensure that users cannot access secrets belonging to other owners - if req.Owner != owner { - s.lggr.Debugf("Secret ID owner: [%s] does not match authorized owner: [%s]", req.Owner, owner) - return nil, errors.New("secret ID owner: " + req.Owner + " does not match authorized owner: " + owner + " at index " + strconv.Itoa(idx)) - } - } - s.lggr.Debugf("Processing authorized and normalized request [%s]", request.String()) return s.handleRequest(ctx, request.RequestId, request) } @@ -263,25 +204,6 @@ func (s *Capability) ListSecretIdentifiers(ctx context.Context, request *vaultco s.lggr.Debugf("Request: [%s] failed validation checks: %s", request.String(), err.Error()) return nil, err } - - authorized, owner, err := s.authorizeListSecrets(ctx, *request) //nolint:govet // The mutex isn't used - if !authorized || err != nil { - s.lggr.Debugf("Request ID[%s] not authorized for owner: %s", request.RequestId, owner) - return nil, errors.New("request ID: " + request.RequestId + " not authorized: " + err.Error()) - } - if !strings.HasPrefix(request.RequestId, owner) { - // Gateway should ensure it prefixes request ids with the owner, to ensure request uniqueness - s.lggr.Debugf("Request ID: [%s] must start with owner address: [%s]", request.RequestId, owner) - return nil, errors.New("request ID: " + request.RequestId + " must start with owner address: " + owner) - } - // Ensures that users cannot access secrets belonging to other owners - request.Owner = owner - if request.Owner != owner { - s.lggr.Debugf("Secret ID owner: [%s] does not match authorized owner: [%s]", request.Owner, owner) - return nil, errors.New("secret ID owner: " + request.Owner + " does not match authorized owner: " + owner) - } - - s.lggr.Debugf("Processing authorized and normalized request [%s]", request.String()) return s.handleRequest(ctx, request.RequestId, request) } @@ -334,76 +256,11 @@ func (s *Capability) handleRequest(ctx context.Context, requestID string, reques } } -func (s *Capability) getOriginalRequestID(transformedRequestID string) (string, error) { - // The transformed RequestID provided to Vault Nodes is of format ::. - // However, the RequestAuthorizer expects just the as the JSONRequest's ID fields, - // since that's what was used by the caller when generating the request digest. - requestIDParts := strings.Split(transformedRequestID, vaulttypes.RequestIDSeparator) - if len(requestIDParts) != 2 { - return "", errors.New("internal error: request ID must be in format ::") - } - return requestIDParts[1], nil -} - -func (s *Capability) authorizeCreateSecrets(ctx context.Context, request vaultcommon.CreateSecretsRequest) (bool, string, error) { //nolint:govet // The mutex isn't used - originalRequestID, err := s.getOriginalRequestID(request.RequestId) - if err != nil { - return false, "", err - } - request.RequestId = originalRequestID - - return s.isAuthorizedRequest(ctx, &request, originalRequestID, vaulttypes.MethodSecretsCreate) -} - -func (s *Capability) authorizeUpdateSecrets(ctx context.Context, request vaultcommon.UpdateSecretsRequest) (bool, string, error) { //nolint:govet // The mutex isn't used - originalRequestID, err := s.getOriginalRequestID(request.RequestId) - if err != nil { - return false, "", err - } - request.RequestId = originalRequestID - return s.isAuthorizedRequest(ctx, &request, originalRequestID, vaulttypes.MethodSecretsUpdate) -} - -func (s *Capability) authorizeDeleteSecrets(ctx context.Context, request vaultcommon.DeleteSecretsRequest) (bool, string, error) { //nolint:govet // The mutex isn't used - originalRequestID, err := s.getOriginalRequestID(request.RequestId) - if err != nil { - return false, "", err - } - request.RequestId = originalRequestID - return s.isAuthorizedRequest(ctx, &request, originalRequestID, vaulttypes.MethodSecretsDelete) -} - -func (s *Capability) authorizeListSecrets(ctx context.Context, request vaultcommon.ListSecretIdentifiersRequest) (bool, string, error) { //nolint:govet // The mutex isn't used - originalRequestID, err := s.getOriginalRequestID(request.RequestId) - if err != nil { - return false, "", err - } - request.RequestId = originalRequestID - return s.isAuthorizedRequest(ctx, &request, originalRequestID, vaulttypes.MethodSecretsList) -} - -func (s *Capability) isAuthorizedRequest(ctx context.Context, request any, requestID, method string) (bool, string, error) { - var params json.RawMessage - params, err := json.Marshal(request) - if err != nil { - return false, "", fmt.Errorf("could not marshal CreateSecretsRequest: %w", err) - } - s.lggr.Debugw("Authorizing request", "method", method, "requestID", requestID) - jsonRequest := jsonrpc.Request[json.RawMessage]{ - Version: jsonrpc.JsonRpcVersion, - ID: requestID, - Method: method, - Params: ¶ms, - } - return s.requestAuthorizer.AuthorizeRequest(ctx, jsonRequest) -} - func NewCapability( lggr logger.Logger, clock clockwork.Clock, expiresAfter time.Duration, handler *requests.Handler[*vaulttypes.Request, *vaulttypes.Response], - requestAuthorizer RequestAuthorizer, capabilitiesRegistry core.CapabilitiesRegistry, publicKey *LazyPublicKey, limitsFactory limits.Factory, @@ -417,7 +274,6 @@ func NewCapability( clock: clock, expiresAfter: expiresAfter, handler: handler, - requestAuthorizer: requestAuthorizer, capabilitiesRegistry: capabilitiesRegistry, publicKey: publicKey, RequestValidator: NewRequestValidator(limiter), diff --git a/core/capabilities/vault/capability_test.go b/core/capabilities/vault/capability_test.go index 66c188c466c..220ac11e95e 100644 --- a/core/capabilities/vault/capability_test.go +++ b/core/capabilities/vault/capability_test.go @@ -10,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/jonboulle/clockwork" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" @@ -24,7 +23,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" coreCapabilities "github.com/smartcontractkit/chainlink/v2/core/capabilities" - vaultcapmocks "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/mocks" "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaulttypes" "github.com/smartcontractkit/chainlink/v2/core/logger" ) @@ -35,10 +33,9 @@ func TestCapability_CapabilityCall(t *testing.T) { expiry := 10 * time.Second store := requests.NewStore[*vaulttypes.Request]() handler := requests.NewHandler[*vaulttypes.Request, *vaulttypes.Response](lggr, store, clock, expiry) - requestAuthorizer := vaultcapmocks.NewRequestAuthorizer(t) reg := coreCapabilities.NewRegistry(lggr) lf := limits.Factory{Settings: cresettings.DefaultGetter} - capability, err := NewCapability(lggr, clock, expiry, handler, requestAuthorizer, reg, nil, lf) + capability, err := NewCapability(lggr, clock, expiry, handler, reg, nil, lf) require.NoError(t, err) servicetest.Run(t, capability) @@ -133,10 +130,9 @@ func TestCapability_CapabilityCall_DuringSubscriptionPhase(t *testing.T) { expiry := 10 * time.Second store := requests.NewStore[*vaulttypes.Request]() handler := requests.NewHandler[*vaulttypes.Request, *vaulttypes.Response](lggr, store, clock, expiry) - requestAuthorizer := vaultcapmocks.NewRequestAuthorizer(t) reg := coreCapabilities.NewRegistry(lggr) lf := limits.Factory{Settings: cresettings.DefaultGetter} - capability, err := NewCapability(lggr, clock, expiry, handler, requestAuthorizer, reg, nil, lf) + capability, err := NewCapability(lggr, clock, expiry, handler, reg, nil, lf) require.NoError(t, err) servicetest.Run(t, capability) @@ -304,10 +300,9 @@ func TestCapability_CapabilityCall_SecretIdentifierOwnerMismatch(t *testing.T) { expiry := 10 * time.Second store := requests.NewStore[*vaulttypes.Request]() handler := requests.NewHandler[*vaulttypes.Request, *vaulttypes.Response](lggr, store, clock, expiry) - requestAuthorizer := vaultcapmocks.NewRequestAuthorizer(t) reg := coreCapabilities.NewRegistry(lggr) lf := limits.Factory{Settings: cresettings.DefaultGetter} - capability, err := NewCapability(lggr, clock, expiry, handler, requestAuthorizer, reg, nil, lf) + capability, err := NewCapability(lggr, clock, expiry, handler, reg, nil, lf) require.NoError(t, err) servicetest.Run(t, capability) @@ -382,10 +377,9 @@ func TestCapability_CapabilityCall_ReturnsIncorrectType(t *testing.T) { expiry := 10 * time.Second store := requests.NewStore[*vaulttypes.Request]() handler := requests.NewHandler[*vaulttypes.Request, *vaulttypes.Response](lggr, store, clock, expiry) - requestAuthorizer := vaultcapmocks.NewRequestAuthorizer(t) reg := coreCapabilities.NewRegistry(lggr) lf := limits.Factory{Settings: cresettings.DefaultGetter} - capability, err := NewCapability(lggr, clock, expiry, handler, requestAuthorizer, reg, nil, lf) + capability, err := NewCapability(lggr, clock, expiry, handler, reg, nil, lf) require.NoError(t, err) servicetest.Run(t, capability) @@ -457,10 +451,9 @@ func TestCapability_CapabilityCall_TimeOut(t *testing.T) { expiry := 10 * time.Second store := requests.NewStore[*vaulttypes.Request]() handler := requests.NewHandler[*vaulttypes.Request, *vaulttypes.Response](lggr, store, fakeClock, expiry) - requestAuthorizer := vaultcapmocks.NewRequestAuthorizer(t) reg := coreCapabilities.NewRegistry(lggr) lf := limits.Factory{Settings: cresettings.DefaultGetter} - capability, err := NewCapability(lggr, fakeClock, expiry, handler, requestAuthorizer, reg, nil, lf) + capability, err := NewCapability(lggr, fakeClock, expiry, handler, reg, nil, lf) require.NoError(t, err) servicetest.Run(t, capability) @@ -1064,24 +1057,6 @@ func TestCapability_CRUD(t *testing.T) { return capability.DeleteSecrets(t.Context(), req) }, }, - { - name: "DeleteSecrets_Invalid_Owner", - response: nil, - error: "secret ID owner: random does not match authorized owner:", - call: func(t *testing.T, capability *Capability) (*vaulttypes.Response, error) { - req := &vault.DeleteSecretsRequest{ - RequestId: requestID, - Ids: []*vault.SecretIdentifier{ - { - Key: "Foo", - Namespace: "Bar", - Owner: "random", - }, - }, - } - return capability.DeleteSecrets(t.Context(), req) - }, - }, { name: "DeleteSecrets_Invalid_Duplicates", error: "duplicate secret ID found", @@ -1180,11 +1155,9 @@ func TestCapability_CRUD(t *testing.T) { expiry := 10 * time.Second store := requests.NewStore[*vaulttypes.Request]() handler := requests.NewHandler[*vaulttypes.Request, *vaulttypes.Response](lggr, store, clock, expiry) - requestAuthorizer := vaultcapmocks.NewRequestAuthorizer(t) - requestAuthorizer.On("AuthorizeRequest", t.Context(), mock.Anything).Return(true, owner, nil).Maybe() reg := coreCapabilities.NewRegistry(lggr) lf := limits.Factory{Settings: cresettings.DefaultGetter} - capability, err := NewCapability(lggr, clock, expiry, handler, requestAuthorizer, reg, lpk, lf) + capability, err := NewCapability(lggr, clock, expiry, handler, reg, lpk, lf) require.NoError(t, err) servicetest.Run(t, capability) @@ -1230,11 +1203,9 @@ func TestCapability_Lifecycle(t *testing.T) { expiry := 10 * time.Second store := requests.NewStore[*vaulttypes.Request]() handler := requests.NewHandler[*vaulttypes.Request, *vaulttypes.Response](lggr, store, clock, expiry) - requestAuthorizer := vaultcapmocks.NewRequestAuthorizer(t) - requestAuthorizer.On("AuthorizeRequest", t.Context(), mock.Anything).Return(true, "owner", nil).Maybe() reg := coreCapabilities.NewRegistry(lggr) lf := limits.Factory{Settings: cresettings.DefaultGetter} - capability, err := NewCapability(lggr, clock, expiry, handler, requestAuthorizer, reg, nil, lf) + capability, err := NewCapability(lggr, clock, expiry, handler, reg, nil, lf) require.NoError(t, err) _, err = reg.GetExecutable(t.Context(), vault.CapabilityID) @@ -1262,11 +1233,10 @@ func TestCapability_PublicKeyGet(t *testing.T) { expiry := 10 * time.Second store := requests.NewStore[*vaulttypes.Request]() handler := requests.NewHandler[*vaulttypes.Request, *vaulttypes.Response](lggr, store, clock, expiry) - requestAuthorizer := vaultcapmocks.NewRequestAuthorizer(t) reg := coreCapabilities.NewRegistry(lggr) lpk := NewLazyPublicKey() lf := limits.Factory{Settings: cresettings.DefaultGetter} - capability, err := NewCapability(lggr, clock, expiry, handler, requestAuthorizer, reg, lpk, lf) + capability, err := NewCapability(lggr, clock, expiry, handler, reg, lpk, lf) require.NoError(t, err) servicetest.Run(t, capability) diff --git a/core/capabilities/vault/digest_replay_guard.go b/core/capabilities/vault/digest_replay_guard.go new file mode 100644 index 00000000000..2ef15221a6f --- /dev/null +++ b/core/capabilities/vault/digest_replay_guard.go @@ -0,0 +1,63 @@ +package vault + +import ( + "errors" + "sync" + "time" +) + +var ErrDigestAlreadySeen = errors.New("request already authorized previously") + +// DigestReplayGuard prevents replay of already-processed requests by tracking +// request digests with expiry timestamps. It is safe for concurrent use. +// +// Used by both the on-chain allowlist flow and the JWT auth flow to ensure +// that a given request digest is only accepted once. +type DigestReplayGuard struct { + mu sync.Mutex + seen map[string]int64 // digest → unix expiry timestamp + nowFunc func() time.Time // injectable for testing +} + +func NewDigestReplayGuard() *DigestReplayGuard { + return &DigestReplayGuard{ + seen: make(map[string]int64), + nowFunc: time.Now, + } +} + +// CheckAndRecord returns ErrDigestAlreadySeen if the digest was previously +// recorded and has not yet expired. Otherwise it records the digest with +// the given expiry timestamp (unix seconds, UTC). +// +// Expired entries are cleaned up on every call. +func (g *DigestReplayGuard) CheckAndRecord(digest string, expiresAtUnix int64) error { + g.mu.Lock() + defer g.mu.Unlock() + + g.clearExpiredLocked() + + if _, exists := g.seen[digest]; exists { + return ErrDigestAlreadySeen + } + + g.seen[digest] = expiresAtUnix + return nil +} + +// ClearExpired removes all entries whose expiry timestamp is in the past. +// Call this to eagerly reclaim memory even when CheckAndRecord is not invoked. +func (g *DigestReplayGuard) ClearExpired() { + g.mu.Lock() + defer g.mu.Unlock() + g.clearExpiredLocked() +} + +func (g *DigestReplayGuard) clearExpiredLocked() { + now := g.nowFunc().UTC().Unix() + for digest, expiry := range g.seen { + if now > expiry { + delete(g.seen, digest) + } + } +} diff --git a/core/capabilities/vault/digest_replay_guard_test.go b/core/capabilities/vault/digest_replay_guard_test.go new file mode 100644 index 00000000000..5d9c9cf64c8 --- /dev/null +++ b/core/capabilities/vault/digest_replay_guard_test.go @@ -0,0 +1,183 @@ +package vault + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDigestReplayGuard_FirstCallSucceeds(t *testing.T) { + guard := NewDigestReplayGuard() + futureExpiry := time.Now().UTC().Unix() + 100 + + err := guard.CheckAndRecord("digest-1", futureExpiry) + require.NoError(t, err) +} + +func TestDigestReplayGuard_DuplicateRejected(t *testing.T) { + guard := NewDigestReplayGuard() + futureExpiry := time.Now().UTC().Unix() + 100 + + err := guard.CheckAndRecord("digest-1", futureExpiry) + require.NoError(t, err) + + err = guard.CheckAndRecord("digest-1", futureExpiry) + require.ErrorIs(t, err, ErrDigestAlreadySeen) +} + +func TestDigestReplayGuard_DifferentDigestsIndependent(t *testing.T) { + guard := NewDigestReplayGuard() + futureExpiry := time.Now().UTC().Unix() + 100 + + require.NoError(t, guard.CheckAndRecord("digest-1", futureExpiry)) + require.NoError(t, guard.CheckAndRecord("digest-2", futureExpiry)) + require.NoError(t, guard.CheckAndRecord("digest-3", futureExpiry)) + + require.ErrorIs(t, guard.CheckAndRecord("digest-1", futureExpiry), ErrDigestAlreadySeen) + require.ErrorIs(t, guard.CheckAndRecord("digest-2", futureExpiry), ErrDigestAlreadySeen) +} + +func TestDigestReplayGuard_ExpiredEntryCleaned(t *testing.T) { + guard := NewDigestReplayGuard() + now := time.Now() + guard.nowFunc = func() time.Time { return now } + + pastExpiry := now.UTC().Unix() - 10 + err := guard.CheckAndRecord("digest-1", pastExpiry) + require.NoError(t, err) + + // Advance time past the expiry — next call should clean up the entry + guard.nowFunc = func() time.Time { return now.Add(20 * time.Second) } + + // Same digest should succeed because the expired entry was cleaned up + err = guard.CheckAndRecord("digest-1", now.Add(20*time.Second).UTC().Unix()+100) + require.NoError(t, err) +} + +func TestDigestReplayGuard_NonExpiredEntryRetained(t *testing.T) { + guard := NewDigestReplayGuard() + now := time.Now() + guard.nowFunc = func() time.Time { return now } + + futureExpiry := now.UTC().Unix() + 100 + require.NoError(t, guard.CheckAndRecord("digest-1", futureExpiry)) + + // Advance time, but NOT past the expiry + guard.nowFunc = func() time.Time { return now.Add(50 * time.Second) } + + err := guard.CheckAndRecord("digest-1", futureExpiry) + require.ErrorIs(t, err, ErrDigestAlreadySeen) +} + +func TestDigestReplayGuard_MixedExpiryCleanup(t *testing.T) { + guard := NewDigestReplayGuard() + now := time.Now() + guard.nowFunc = func() time.Time { return now } + + shortExpiry := now.UTC().Unix() + 10 + longExpiry := now.UTC().Unix() + 200 + + require.NoError(t, guard.CheckAndRecord("short-lived", shortExpiry)) + require.NoError(t, guard.CheckAndRecord("long-lived", longExpiry)) + + // Advance past short expiry but before long expiry + guard.nowFunc = func() time.Time { return now.Add(50 * time.Second) } + + // Short-lived should be re-recordable (cleaned up) + require.NoError(t, guard.CheckAndRecord("short-lived", now.Add(50*time.Second).UTC().Unix()+100)) + + // Long-lived should still be rejected + require.ErrorIs(t, guard.CheckAndRecord("long-lived", longExpiry), ErrDigestAlreadySeen) +} + +func TestDigestReplayGuard_ConcurrentAccess(t *testing.T) { + guard := NewDigestReplayGuard() + futureExpiry := time.Now().UTC().Unix() + 100 + + const goroutines = 100 + results := make([]error, goroutines) + var wg sync.WaitGroup + wg.Add(goroutines) + + for i := range goroutines { + go func(idx int) { + defer wg.Done() + results[idx] = guard.CheckAndRecord("same-digest", futureExpiry) + }(i) + } + wg.Wait() + + successCount := 0 + duplicateCount := 0 + for _, err := range results { + if err == nil { + successCount++ + } else { + require.ErrorIs(t, err, ErrDigestAlreadySeen) + duplicateCount++ + } + } + + assert.Equal(t, 1, successCount, "exactly one goroutine should succeed") + assert.Equal(t, goroutines-1, duplicateCount, "all others should be rejected as duplicates") +} + +func TestDigestReplayGuard_ConcurrentDifferentDigests(t *testing.T) { + guard := NewDigestReplayGuard() + futureExpiry := time.Now().UTC().Unix() + 100 + + const goroutines = 50 + var wg sync.WaitGroup + wg.Add(goroutines) + + errors := make([]error, goroutines) + for i := range goroutines { + go func(idx int) { + defer wg.Done() + digest := "digest-" + string(rune('A'+idx)) + errors[idx] = guard.CheckAndRecord(digest, futureExpiry) + }(i) + } + wg.Wait() + + for i, err := range errors { + assert.NoError(t, err, "goroutine %d should succeed for unique digest", i) + } +} + +func TestDigestReplayGuard_ClearExpiredIndependently(t *testing.T) { + guard := NewDigestReplayGuard() + now := time.Now() + guard.nowFunc = func() time.Time { return now } + + shortExpiry := now.UTC().Unix() + 5 + longExpiry := now.UTC().Unix() + 200 + + require.NoError(t, guard.CheckAndRecord("ephemeral", shortExpiry)) + require.NoError(t, guard.CheckAndRecord("durable", longExpiry)) + + // Advance past the short expiry + guard.nowFunc = func() time.Time { return now.Add(30 * time.Second) } + + // ClearExpired should prune without needing a CheckAndRecord call + guard.ClearExpired() + + guard.mu.Lock() + _, ephemeralPresent := guard.seen["ephemeral"] + _, durablePresent := guard.seen["durable"] + guard.mu.Unlock() + + assert.False(t, ephemeralPresent, "expired entry should have been pruned") + assert.True(t, durablePresent, "non-expired entry should remain") +} + +func TestDigestReplayGuard_EmptyDigest(t *testing.T) { + guard := NewDigestReplayGuard() + futureExpiry := time.Now().UTC().Unix() + 100 + + require.NoError(t, guard.CheckAndRecord("", futureExpiry)) + require.ErrorIs(t, guard.CheckAndRecord("", futureExpiry), ErrDigestAlreadySeen) +} diff --git a/core/capabilities/vault/gw_handler.go b/core/capabilities/vault/gw_handler.go index 29378a767fd..94b1563142e 100644 --- a/core/capabilities/vault/gw_handler.go +++ b/core/capabilities/vault/gw_handler.go @@ -2,15 +2,13 @@ package vault import ( "context" - "encoding/hex" "encoding/json" "errors" "fmt" - "sort" + "strings" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" - "google.golang.org/protobuf/proto" "github.com/smartcontractkit/chainlink-common/pkg/beholder" vaultcommon "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/vault" @@ -62,25 +60,25 @@ type GatewayHandler struct { services.Service eng *services.Engine - capRegistry core.CapabilitiesRegistry - secretsService vaulttypes.SecretsService - gatewayConnector gatewayConnector - lggr logger.Logger - metrics *metrics + secretsService vaulttypes.SecretsService + gatewayConnector gatewayConnector + requestAuthorizer RequestAuthorizer + lggr logger.Logger + metrics *metrics } -func NewGatewayHandler(capabilitiesRegistry core.CapabilitiesRegistry, secretsService vaulttypes.SecretsService, connector gatewayConnector, lggr logger.Logger) (*GatewayHandler, error) { +func NewGatewayHandler(secretsService vaulttypes.SecretsService, connector gatewayConnector, requestAuthorizer RequestAuthorizer, lggr logger.Logger) (*GatewayHandler, error) { metrics, err := newMetrics() if err != nil { return nil, fmt.Errorf("failed to create metrics: %w", err) } gh := &GatewayHandler{ - capRegistry: capabilitiesRegistry, - secretsService: secretsService, - gatewayConnector: connector, - lggr: lggr.Named(HandlerName), - metrics: metrics, + secretsService: secretsService, + gatewayConnector: connector, + requestAuthorizer: requestAuthorizer, + lggr: lggr.Named(HandlerName), + metrics: metrics, } gh.Service, gh.eng = services.Config{ Name: "GatewayHandler", @@ -109,7 +107,7 @@ func (h *GatewayHandler) ID(ctx context.Context) (string, error) { } func (h *GatewayHandler) Methods() []string { - return vaulttypes.GetSupportedMethods(h.lggr) + return vaulttypes.Methods } func (h *GatewayHandler) HandleGatewayMessage(ctx context.Context, gatewayID string, req *jsonrpc.Request[json.RawMessage]) (err error) { @@ -118,15 +116,33 @@ func (h *GatewayHandler) HandleGatewayMessage(ctx context.Context, gatewayID str var response *jsonrpc.Response[json.RawMessage] switch req.Method { case vaulttypes.MethodSecretsCreate: - response = h.handleSecretsCreate(ctx, gatewayID, req) - case vaulttypes.MethodSecretsGet: - response = h.handleSecretsGet(ctx, gatewayID, req) + owner, authErr := h.authorizeAndPrefixRequest(ctx, req) + if authErr != nil { + response = h.errorResponse(ctx, gatewayID, req, api.FatalError, authErr) + break + } + response = h.handleSecretsCreate(ctx, gatewayID, req, owner) case vaulttypes.MethodSecretsUpdate: - response = h.handleSecretsUpdate(ctx, gatewayID, req) + owner, authErr := h.authorizeAndPrefixRequest(ctx, req) + if authErr != nil { + response = h.errorResponse(ctx, gatewayID, req, api.FatalError, authErr) + break + } + response = h.handleSecretsUpdate(ctx, gatewayID, req, owner) case vaulttypes.MethodSecretsDelete: - response = h.handleSecretsDelete(ctx, gatewayID, req) + owner, authErr := h.authorizeAndPrefixRequest(ctx, req) + if authErr != nil { + response = h.errorResponse(ctx, gatewayID, req, api.FatalError, authErr) + break + } + response = h.handleSecretsDelete(ctx, gatewayID, req, owner) case vaulttypes.MethodSecretsList: - response = h.handleSecretsList(ctx, gatewayID, req) + owner, authErr := h.authorizeAndPrefixRequest(ctx, req) + if authErr != nil { + response = h.errorResponse(ctx, gatewayID, req, api.FatalError, authErr) + break + } + response = h.handleSecretsList(ctx, gatewayID, req, owner) case vaulttypes.MethodPublicKeyGet: response = h.handlePublicKeyGet(ctx, gatewayID, req) default: @@ -145,33 +161,110 @@ func (h *GatewayHandler) HandleGatewayMessage(ctx context.Context, gatewayID str return nil } -func (h *GatewayHandler) handleSecretsCreate(ctx context.Context, gatewayID string, req *jsonrpc.Request[json.RawMessage]) *jsonrpc.Response[json.RawMessage] { - vaultCapRequest := vaultcommon.CreateSecretsRequest{} - if err := json.Unmarshal(*req.Params, &vaultCapRequest); err != nil { - return h.errorResponse(ctx, gatewayID, req, api.UserMessageParseError, err) +func (h *GatewayHandler) authorizeAndPrefixRequest(ctx context.Context, req *jsonrpc.Request[json.RawMessage]) (string, error) { + if h.requestAuthorizer == nil { + err := errors.New("request authorizer is nil") + h.lggr.Errorw("failed to authorize gateway request", "method", req.Method, "requestID", req.ID, "error", err) + return "", err } - vaultCapRequest.RequestId = req.ID + originalRequestID := req.ID + incomingOwner := "" + if idx := strings.Index(req.ID, vaulttypes.RequestIDSeparator); idx != -1 { + incomingOwner = req.ID[:idx] + originalRequestID = req.ID[idx+len(vaulttypes.RequestIDSeparator):] + } - vaultCapResponse, err := h.secretsService.CreateSecrets(ctx, &vaultCapRequest) - if err != nil { - return h.errorResponse(ctx, gatewayID, req, api.FatalError, err) + authReq := *req + authReq.ID = originalRequestID + if err := stripPrefixedRequestIDFromParams(&authReq, originalRequestID); err != nil { + h.lggr.Errorw("failed to normalize gateway request for authorization", "method", req.Method, "requestID", originalRequestID, "error", err) + return "", err } - jsonResponse, err := toJSONResponse(vaultCapResponse, req.Method) + h.lggr.Debugw("authorizing gateway request", "method", req.Method, "requestID", originalRequestID) + isAuthorized, owner, err := h.requestAuthorizer.AuthorizeRequest(ctx, authReq) + if !isAuthorized { + authErr := fmt.Errorf("request not authorized: %w", err) + h.lggr.Errorw("gateway request authorization failed", "method", req.Method, "requestID", originalRequestID, "owner", owner, "error", authErr) + return "", authErr + } + if incomingOwner != "" && normalizeOwner(incomingOwner) != normalizeOwner(owner) { + prefixErr := fmt.Errorf("request owner prefix %q does not match authorized owner %q", incomingOwner, owner) + h.lggr.Errorw("gateway request owner prefix mismatch", "method", req.Method, "requestID", originalRequestID, "incomingOwner", incomingOwner, "authorizedOwner", owner, "error", prefixErr) + return "", prefixErr + } + + req.ID = owner + vaulttypes.RequestIDSeparator + originalRequestID + h.lggr.Debugw("authorized gateway request", "method", req.Method, "requestID", req.ID, "owner", owner) + return owner, nil +} + +func stripPrefixedRequestIDFromParams(req *jsonrpc.Request[json.RawMessage], originalRequestID string) error { + if req.Params == nil { + return nil + } + + switch req.Method { + case vaulttypes.MethodSecretsCreate: + parsed := &vaultcommon.CreateSecretsRequest{} + if err := json.Unmarshal(*req.Params, parsed); err != nil { + return err + } + parsed.RequestId = originalRequestID + return rewriteRequestParams(req, parsed) + case vaulttypes.MethodSecretsUpdate: + parsed := &vaultcommon.UpdateSecretsRequest{} + if err := json.Unmarshal(*req.Params, parsed); err != nil { + return err + } + parsed.RequestId = originalRequestID + return rewriteRequestParams(req, parsed) + case vaulttypes.MethodSecretsDelete: + parsed := &vaultcommon.DeleteSecretsRequest{} + if err := json.Unmarshal(*req.Params, parsed); err != nil { + return err + } + parsed.RequestId = originalRequestID + return rewriteRequestParams(req, parsed) + case vaulttypes.MethodSecretsList: + parsed := &vaultcommon.ListSecretIdentifiersRequest{} + if err := json.Unmarshal(*req.Params, parsed); err != nil { + return err + } + parsed.RequestId = originalRequestID + return rewriteRequestParams(req, parsed) + default: + return nil + } +} + +func rewriteRequestParams(req *jsonrpc.Request[json.RawMessage], payload any) error { + params, err := json.Marshal(payload) if err != nil { - return h.errorResponse(ctx, gatewayID, req, api.NodeReponseEncodingError, err) + return err } - return jsonResponse + raw := json.RawMessage(params) + req.Params = &raw + return nil } -func (h *GatewayHandler) handleSecretsUpdate(ctx context.Context, gatewayID string, req *jsonrpc.Request[json.RawMessage]) *jsonrpc.Response[json.RawMessage] { - vaultCapRequest := vaultcommon.UpdateSecretsRequest{} +func (h *GatewayHandler) handleSecretsCreate(ctx context.Context, gatewayID string, req *jsonrpc.Request[json.RawMessage], owner string) *jsonrpc.Response[json.RawMessage] { + vaultCapRequest := vaultcommon.CreateSecretsRequest{} if err := json.Unmarshal(*req.Params, &vaultCapRequest); err != nil { return h.errorResponse(ctx, gatewayID, req, api.UserMessageParseError, err) } - vaultCapResponse, err := h.secretsService.UpdateSecrets(ctx, &vaultCapRequest) + vaultCapRequest.RequestId = req.ID + for idx, encryptedSecret := range vaultCapRequest.EncryptedSecrets { + if encryptedSecret != nil && encryptedSecret.Id != nil && normalizeOwner(encryptedSecret.Id.Owner) != normalizeOwner(owner) { + h.lggr.Debugw("create secrets request owner mismatch", "requestID", req.ID, "secretOwner", encryptedSecret.Id.Owner, "authorizedOwner", owner, "index", idx) + return h.errorResponse(ctx, gatewayID, req, api.FatalError, fmt.Errorf("secret ID owner %q does not match authorized owner %q at index %d", encryptedSecret.Id.Owner, owner, idx)) + } + } + + h.lggr.Debugf("Processing authorized and normalized create secrets request [%s]", vaultCapRequest.String()) + vaultCapResponse, err := h.secretsService.CreateSecrets(ctx, &vaultCapRequest) if err != nil { return h.errorResponse(ctx, gatewayID, req, api.FatalError, err) } @@ -183,57 +276,46 @@ func (h *GatewayHandler) handleSecretsUpdate(ctx context.Context, gatewayID stri return jsonResponse } -func (h *GatewayHandler) handleSecretsGet(ctx context.Context, gatewayID string, req *jsonrpc.Request[json.RawMessage]) *jsonrpc.Response[json.RawMessage] { - var request vaultcommon.GetSecretsRequest - if err := json.Unmarshal(*req.Params, &request); err != nil { +func (h *GatewayHandler) handleSecretsUpdate(ctx context.Context, gatewayID string, req *jsonrpc.Request[json.RawMessage], owner string) *jsonrpc.Response[json.RawMessage] { + vaultCapRequest := vaultcommon.UpdateSecretsRequest{} + if err := json.Unmarshal(*req.Params, &vaultCapRequest); err != nil { return h.errorResponse(ctx, gatewayID, req, api.UserMessageParseError, err) } - encryptionKeys, err := h.getEncryptionKeys(ctx) - if err != nil { - return h.errorResponse(ctx, gatewayID, req, api.FatalError, err) - } - getSecretsRequest := vaultcommon.GetSecretsRequest{} - for _, reqItem := range request.Requests { - getSecretsRequest.Requests = append(getSecretsRequest.Requests, &vaultcommon.SecretRequest{ - Id: &vaultcommon.SecretIdentifier{ - Owner: reqItem.Id.Owner, - Namespace: reqItem.Id.Namespace, - Key: reqItem.Id.Key, - }, - EncryptionKeys: encryptionKeys, - }) - } - vaultCapResponse, err := h.secretsService.GetSecrets(ctx, req.ID, &getSecretsRequest) - if err != nil { - return h.errorResponse(ctx, gatewayID, req, api.FatalError, err) + vaultCapRequest.RequestId = req.ID + for idx, encryptedSecret := range vaultCapRequest.EncryptedSecrets { + if encryptedSecret != nil && encryptedSecret.Id != nil && normalizeOwner(encryptedSecret.Id.Owner) != normalizeOwner(owner) { + h.lggr.Debugw("update secrets request owner mismatch", "requestID", req.ID, "secretOwner", encryptedSecret.Id.Owner, "authorizedOwner", owner, "index", idx) + return h.errorResponse(ctx, gatewayID, req, api.FatalError, fmt.Errorf("secret ID owner %q does not match authorized owner %q at index %d", encryptedSecret.Id.Owner, owner, idx)) + } } - vaultResponseProto := &vaultcommon.GetSecretsResponse{} - err = proto.Unmarshal(vaultCapResponse.Payload, vaultResponseProto) + h.lggr.Debugf("Processing authorized and normalized update secrets request [%s]", vaultCapRequest.String()) + vaultCapResponse, err := h.secretsService.UpdateSecrets(ctx, &vaultCapRequest) if err != nil { - h.lggr.Errorf("Debugging: handleSecretsCreate failed to unmarshal response: %s. Payload was: %s", err.Error(), string(vaultCapResponse.Payload)) - return h.errorResponse(ctx, gatewayID, req, api.NodeReponseEncodingError, err) + return h.errorResponse(ctx, gatewayID, req, api.FatalError, err) } - vaultAPIResponseBytes, err := json.Marshal(vaultResponseProto) + jsonResponse, err := toJSONResponse(vaultCapResponse, req.Method) if err != nil { return h.errorResponse(ctx, gatewayID, req, api.NodeReponseEncodingError, err) } - vaultAPIResponseJSON := json.RawMessage(vaultAPIResponseBytes) - return &jsonrpc.Response[json.RawMessage]{ - Version: jsonrpc.JsonRpcVersion, - ID: req.ID, - Method: req.Method, - Result: &vaultAPIResponseJSON, - } + return jsonResponse } -func (h *GatewayHandler) handleSecretsDelete(ctx context.Context, gatewayID string, req *jsonrpc.Request[json.RawMessage]) *jsonrpc.Response[json.RawMessage] { +func (h *GatewayHandler) handleSecretsDelete(ctx context.Context, gatewayID string, req *jsonrpc.Request[json.RawMessage], owner string) *jsonrpc.Response[json.RawMessage] { r := &vaultcommon.DeleteSecretsRequest{} if err := json.Unmarshal(*req.Params, r); err != nil { return h.errorResponse(ctx, gatewayID, req, api.UserMessageParseError, err) } + r.RequestId = req.ID + for idx, secretID := range r.Ids { + if secretID != nil && normalizeOwner(secretID.Owner) != normalizeOwner(owner) { + h.lggr.Debugw("delete secrets request owner mismatch", "requestID", req.ID, "secretOwner", secretID.Owner, "authorizedOwner", owner, "index", idx) + return h.errorResponse(ctx, gatewayID, req, api.FatalError, fmt.Errorf("secret ID owner %q does not match authorized owner %q at index %d", secretID.Owner, owner, idx)) + } + } + h.lggr.Debugf("Processing authorized and normalized delete secrets request [%s]", r.String()) resp, err := h.secretsService.DeleteSecrets(ctx, r) if err != nil { return h.errorResponse(ctx, gatewayID, req, api.HandlerError, fmt.Errorf("failed to delete secrets: %w", err)) @@ -252,12 +334,15 @@ func (h *GatewayHandler) handleSecretsDelete(ctx context.Context, gatewayID stri } } -func (h *GatewayHandler) handleSecretsList(ctx context.Context, gatewayID string, req *jsonrpc.Request[json.RawMessage]) *jsonrpc.Response[json.RawMessage] { +func (h *GatewayHandler) handleSecretsList(ctx context.Context, gatewayID string, req *jsonrpc.Request[json.RawMessage], owner string) *jsonrpc.Response[json.RawMessage] { r := &vaultcommon.ListSecretIdentifiersRequest{} if err := json.Unmarshal(*req.Params, r); err != nil { return h.errorResponse(ctx, gatewayID, req, api.UserMessageParseError, err) } + r.RequestId = req.ID + r.Owner = owner + h.lggr.Debugf("Processing authorized and normalized list secrets request [%s]", r.String()) resp, err := h.secretsService.ListSecretIdentifiers(ctx, r) if err != nil { return h.errorResponse(ctx, gatewayID, req, api.HandlerError, fmt.Errorf("failed to list secret identifiers: %w", err)) @@ -324,26 +409,6 @@ func (h *GatewayHandler) errorResponse( } } -// getEncryptionKeys retrieves the encryption keys of all members in the Workflow DON. -func (h *GatewayHandler) getEncryptionKeys(ctx context.Context) ([]string, error) { - myNode, err := h.capRegistry.LocalNode(ctx) - if err != nil { - return nil, errors.New("failed to get local node from registry" + err.Error()) - } - - encryptionKeys := make([]string, 0, len(myNode.WorkflowDON.Members)) - for _, peerID := range myNode.WorkflowDON.Members { - peerNode, err := h.capRegistry.NodeByPeerID(ctx, peerID) - if err != nil { - return nil, errors.New("failed to get node info for peerID: " + peerID.String() + " - " + err.Error()) - } - encryptionKeys = append(encryptionKeys, hex.EncodeToString(peerNode.EncryptionPublicKey[:])) - } - // Sort the encryption keys to ensure consistent ordering across all nodes. - sort.Strings(encryptionKeys) - return encryptionKeys, nil -} - func toJSONResponse(vaultCapResponse *vaulttypes.Response, method string) (*jsonrpc.Response[json.RawMessage], error) { vaultResponseBytes, err := vaultCapResponse.ToJSONRPCResult() if err != nil { diff --git a/core/capabilities/vault/gw_handler_test.go b/core/capabilities/vault/gw_handler_test.go index b2c5650952f..293544e9d0a 100644 --- a/core/capabilities/vault/gw_handler_test.go +++ b/core/capabilities/vault/gw_handler_test.go @@ -11,8 +11,8 @@ import ( vaultcommon "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/vault" jsonrpc "github.com/smartcontractkit/chainlink-common/pkg/jsonrpc2" - core_mocks "github.com/smartcontractkit/chainlink-common/pkg/types/core/mocks" vaultcap "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault" + vaultcapmocks "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/mocks" "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaulttypes" vaulttypesmocks "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaulttypes/mocks" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -26,16 +26,21 @@ func TestGatewayHandler_HandleGatewayMessage(t *testing.T) { tests := []struct { name string - setupMocks func(*vaulttypesmocks.SecretsService, *connector_mocks.GatewayConnector) + setupMocks func(*vaulttypesmocks.SecretsService, *connector_mocks.GatewayConnector, *vaultcapmocks.RequestAuthorizer) request *jsonrpc.Request[json.RawMessage] expectedError bool }{ { name: "success - create secrets", - setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector) { + setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector, ra *vaultcapmocks.RequestAuthorizer) { + ra.EXPECT().AuthorizeRequest(mock.Anything, mock.MatchedBy(func(req jsonrpc.Request[json.RawMessage]) bool { + return req.Method == vaulttypes.MethodSecretsCreate && req.ID == "1" + })).Return(true, "0xabc", nil) ss.EXPECT().CreateSecrets(mock.Anything, mock.MatchedBy(func(req *vaultcommon.CreateSecretsRequest) bool { return len(req.EncryptedSecrets) == 1 && - req.EncryptedSecrets[0].Id.Key == "test-secret" + req.EncryptedSecrets[0].Id.Key == "test-secret" && + req.EncryptedSecrets[0].Id.Owner == "0xAbC" && + req.RequestId == "0xabc"+vaulttypes.RequestIDSeparator+"1" })).Return(&vaulttypes.Response{ID: "test-secret"}, nil) gc.On("SendToGateway", mock.Anything, "gateway-1", mock.MatchedBy(func(resp *jsonrpc.Response[json.RawMessage]) bool { @@ -51,7 +56,8 @@ func TestGatewayHandler_HandleGatewayMessage(t *testing.T) { EncryptedSecrets: []*vaultcommon.EncryptedSecret{ { Id: &vaultcommon.SecretIdentifier{ - Key: "test-secret", + Key: "test-secret", + Owner: "0xAbC", }, EncryptedValue: "encrypted-value", }, @@ -65,7 +71,8 @@ func TestGatewayHandler_HandleGatewayMessage(t *testing.T) { }, { name: "failure - service error", - setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector) { + setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector, ra *vaultcapmocks.RequestAuthorizer) { + ra.EXPECT().AuthorizeRequest(mock.Anything, mock.Anything).Return(true, "0xabc", nil) ss.EXPECT().CreateSecrets(mock.Anything, mock.Anything).Return(nil, errors.New("service error")) gc.On("SendToGateway", mock.Anything, "gateway-1", mock.MatchedBy(func(resp *jsonrpc.Response[json.RawMessage]) bool { @@ -82,7 +89,8 @@ func TestGatewayHandler_HandleGatewayMessage(t *testing.T) { EncryptedSecrets: []*vaultcommon.EncryptedSecret{ { Id: &vaultcommon.SecretIdentifier{ - Key: "test-secret", + Key: "test-secret", + Owner: "0xAbC", }, EncryptedValue: "encrypted-value", }, @@ -96,7 +104,7 @@ func TestGatewayHandler_HandleGatewayMessage(t *testing.T) { }, { name: "failure - invalid method", - setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector) { + setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector, ra *vaultcapmocks.RequestAuthorizer) { gc.On("SendToGateway", mock.Anything, "gateway-1", mock.MatchedBy(func(resp *jsonrpc.Response[json.RawMessage]) bool { return resp.Error != nil && resp.Error.Code == api.ToJSONRPCErrorCode(api.UnsupportedMethodError) @@ -110,10 +118,10 @@ func TestGatewayHandler_HandleGatewayMessage(t *testing.T) { }, { name: "failure - invalid request params", - setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector) { + setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector, ra *vaultcapmocks.RequestAuthorizer) { gc.On("SendToGateway", mock.Anything, "gateway-1", mock.MatchedBy(func(resp *jsonrpc.Response[json.RawMessage]) bool { return resp.Error != nil && - resp.Error.Code == api.ToJSONRPCErrorCode(api.UserMessageParseError) + resp.Error.Code == api.ToJSONRPCErrorCode(api.FatalError) })).Return(nil) }, request: &jsonrpc.Request[json.RawMessage]{ @@ -128,12 +136,16 @@ func TestGatewayHandler_HandleGatewayMessage(t *testing.T) { }, { name: "success - delete secrets", - setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector) { + setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector, ra *vaultcapmocks.RequestAuthorizer) { + ra.EXPECT().AuthorizeRequest(mock.Anything, mock.MatchedBy(func(req jsonrpc.Request[json.RawMessage]) bool { + return req.Method == vaulttypes.MethodSecretsDelete && req.ID == "1" + })).Return(true, "0xabc", nil) ss.EXPECT().DeleteSecrets(mock.Anything, mock.MatchedBy(func(req *vaultcommon.DeleteSecretsRequest) bool { return len(req.Ids) == 1 && req.Ids[0].Key == "Foo" && req.Ids[0].Namespace == "Bar" && - req.Ids[0].Owner == "Owner" + req.Ids[0].Owner == "0xAbC" && + req.RequestId == "0xabc"+vaulttypes.RequestIDSeparator+"1" })).Return(&vaulttypes.Response{ID: "test-secret"}, nil) gc.On("SendToGateway", mock.Anything, "gateway-1", mock.MatchedBy(func(resp *jsonrpc.Response[json.RawMessage]) bool { @@ -148,10 +160,119 @@ func TestGatewayHandler_HandleGatewayMessage(t *testing.T) { RequestId: "test-secret", Ids: []*vaultcommon.SecretIdentifier{ { - Key: "Foo", Namespace: "Bar", - Owner: "Owner", + Owner: "0xAbC", + }, + }, + }) + raw := json.RawMessage(params) + return &raw + }(), + }, + expectedError: false, + }, + { + name: "failure - unauthorized request", + setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector, ra *vaultcapmocks.RequestAuthorizer) { + ra.EXPECT().AuthorizeRequest(mock.Anything, mock.Anything).Return(false, "", errors.New("not allowlisted")) + gc.On("SendToGateway", mock.Anything, "gateway-1", mock.MatchedBy(func(resp *jsonrpc.Response[json.RawMessage]) bool { + return resp.Error != nil && + resp.Error.Code == api.ToJSONRPCErrorCode(api.FatalError) && + resp.Error.Message == "request not authorized: not allowlisted" + })).Return(nil) + }, + request: &jsonrpc.Request[json.RawMessage]{ + Method: vaulttypes.MethodSecretsCreate, + ID: "1", + Params: func() *json.RawMessage { + params, _ := json.Marshal(vaultcommon.CreateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + { + Id: &vaultcommon.SecretIdentifier{ + Key: "test-secret", + Owner: "0xAbC", + }, + EncryptedValue: "encrypted-value", + }, + }, + }) + raw := json.RawMessage(params) + return &raw + }(), + }, + expectedError: false, + }, + { + name: "success - strips owner prefix from forwarded request before authorization", + setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector, ra *vaultcapmocks.RequestAuthorizer) { + ra.EXPECT().AuthorizeRequest(mock.Anything, mock.MatchedBy(func(req jsonrpc.Request[json.RawMessage]) bool { + if req.Method != vaulttypes.MethodSecretsCreate || req.ID != "1" || req.Params == nil { + return false + } + + var parsed vaultcommon.CreateSecretsRequest + if err := json.Unmarshal(*req.Params, &parsed); err != nil { + return false + } + + return parsed.RequestId == "1" && + len(parsed.EncryptedSecrets) == 1 && + parsed.EncryptedSecrets[0].Id != nil && + parsed.EncryptedSecrets[0].Id.Owner == "0xAbC" + })).Return(true, "0xabc", nil) + ss.EXPECT().CreateSecrets(mock.Anything, mock.MatchedBy(func(req *vaultcommon.CreateSecretsRequest) bool { + return req.RequestId == "0xabc"+vaulttypes.RequestIDSeparator+"1" + })).Return(&vaulttypes.Response{ID: "test-secret"}, nil) + + gc.On("SendToGateway", mock.Anything, "gateway-1", mock.MatchedBy(func(resp *jsonrpc.Response[json.RawMessage]) bool { + return resp.Error == nil + })).Return(nil) + }, + request: &jsonrpc.Request[json.RawMessage]{ + Method: vaulttypes.MethodSecretsCreate, + ID: "0xAbC" + vaulttypes.RequestIDSeparator + "1", + Params: func() *json.RawMessage { + params, _ := json.Marshal(vaultcommon.CreateSecretsRequest{ + RequestId: "0xAbC" + vaulttypes.RequestIDSeparator + "1", + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + { + Id: &vaultcommon.SecretIdentifier{ + Key: "test-secret", + Owner: "0xAbC", + }, + EncryptedValue: "encrypted-value", + }, + }, + }) + raw := json.RawMessage(params) + return &raw + }(), + }, + expectedError: false, + }, + { + name: "failure - owner mismatch against authorized owner", + setupMocks: func(ss *vaulttypesmocks.SecretsService, gc *connector_mocks.GatewayConnector, ra *vaultcapmocks.RequestAuthorizer) { + ra.EXPECT().AuthorizeRequest(mock.Anything, mock.Anything).Return(true, "0xdef", nil) + gc.On("SendToGateway", mock.Anything, "gateway-1", mock.MatchedBy(func(resp *jsonrpc.Response[json.RawMessage]) bool { + return resp.Error != nil && + resp.Error.Code == api.ToJSONRPCErrorCode(api.FatalError) && + resp.Error.Message == `secret ID owner "0xabc" does not match authorized owner "0xdef" at index 0` + })).Return(nil) + }, + request: &jsonrpc.Request[json.RawMessage]{ + Method: vaulttypes.MethodSecretsCreate, + ID: "1", + Params: func() *json.RawMessage { + params, _ := json.Marshal(vaultcommon.CreateSecretsRequest{ + EncryptedSecrets: []*vaultcommon.EncryptedSecret{ + { + Id: &vaultcommon.SecretIdentifier{ + Key: "test-secret", + Owner: "0xabc", + }, + EncryptedValue: "encrypted-value", }, }, }) @@ -167,11 +288,11 @@ func TestGatewayHandler_HandleGatewayMessage(t *testing.T) { t.Run(tt.name, func(t *testing.T) { secretsService := vaulttypesmocks.NewSecretsService(t) gwConnector := connector_mocks.NewGatewayConnector(t) - capRegistry := core_mocks.NewCapabilitiesRegistry(t) + requestAuthorizer := vaultcapmocks.NewRequestAuthorizer(t) - tt.setupMocks(secretsService, gwConnector) + tt.setupMocks(secretsService, gwConnector, requestAuthorizer) - handler, err := vaultcap.NewGatewayHandler(capRegistry, secretsService, gwConnector, lggr) + handler, err := vaultcap.NewGatewayHandler(secretsService, gwConnector, requestAuthorizer, lggr) require.NoError(t, err) err = handler.HandleGatewayMessage(ctx, "gateway-1", tt.request) @@ -191,19 +312,19 @@ func TestGatewayHandler_Lifecycle(t *testing.T) { secretsService := vaulttypesmocks.NewSecretsService(t) gwConnector := connector_mocks.NewGatewayConnector(t) - capRegistry := core_mocks.NewCapabilitiesRegistry(t) + requestAuthorizer := vaultcapmocks.NewRequestAuthorizer(t) - handler, err := vaultcap.NewGatewayHandler(capRegistry, secretsService, gwConnector, lggr) + handler, err := vaultcap.NewGatewayHandler(secretsService, gwConnector, requestAuthorizer, lggr) require.NoError(t, err) t.Run("start", func(t *testing.T) { - gwConnector.On("AddHandler", mock.Anything, vaulttypes.GetSupportedMethods(lggr), handler).Return(nil).Once() + gwConnector.On("AddHandler", mock.Anything, vaulttypes.Methods, handler).Return(nil).Once() err := handler.Start(ctx) require.NoError(t, err) }) t.Run("close", func(t *testing.T) { - gwConnector.On("RemoveHandler", mock.Anything, vaulttypes.GetSupportedMethods(lggr)).Return(nil).Once() + gwConnector.On("RemoveHandler", mock.Anything, vaulttypes.Methods).Return(nil).Once() err := handler.Close() require.NoError(t, err) }) diff --git a/core/capabilities/vault/request_authorizer.go b/core/capabilities/vault/request_authorizer.go index aee595cce10..307a2ba4412 100644 --- a/core/capabilities/vault/request_authorizer.go +++ b/core/capabilities/vault/request_authorizer.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "sync" "time" jsonrpc "github.com/smartcontractkit/chainlink-common/pkg/jsonrpc2" @@ -19,16 +18,15 @@ type RequestAuthorizer interface { AuthorizeRequest(ctx context.Context, req jsonrpc.Request[json.RawMessage]) (isAuthorized bool, owner string, err error) } type requestAuthorizer struct { - workflowRegistrySyncer workflowsyncerv2.WorkflowRegistrySyncer - alreadyAuthorizedRequests map[string]int64 - alreadyAuthorizedMutex sync.Mutex - lggr logger.Logger + workflowRegistrySyncer workflowsyncerv2.WorkflowRegistrySyncer + replayGuard *DigestReplayGuard + lggr logger.Logger } // AuthorizeRequest authorizes a request based on the request digest and the allowlisted requests. // It does NOT check if the request method is allowed. func (r *requestAuthorizer) AuthorizeRequest(ctx context.Context, req jsonrpc.Request[json.RawMessage]) (isAuthorized bool, owner string, err error) { - defer r.clearExpiredAuthorizedRequests() + defer r.replayGuard.ClearExpired() r.lggr.Infow("AuthorizeRequest", "method", req.Method, "requestID", req.ID) requestDigest, err := req.Digest() if err != nil { @@ -61,31 +59,21 @@ func (r *requestAuthorizer) AuthorizeRequest(ctx context.Context, req jsonrpc.Re "allowedRequestsStrs", allowedRequestsStrs) return false, "", errors.New("request not allowlisted") } - authorizedRequestStr := string(allowlistedRequest.RequestDigest[:]) - r.alreadyAuthorizedMutex.Lock() - defer r.alreadyAuthorizedMutex.Unlock() - if r.alreadyAuthorizedRequests[authorizedRequestStr] > 0 { - r.lggr.Infow("AuthorizeRequest already authorized previously", "method", req.Method, "requestID", req.ID, "authorizedRequestStr", authorizedRequestStr) - return false, "", errors.New("request already authorized previously") - } if time.Now().UTC().Unix() > int64(allowlistedRequest.ExpiryTimestamp) { + authorizedRequestStr := string(allowlistedRequest.RequestDigest[:]) r.lggr.Infow("AuthorizeRequest expired authorization", "method", req.Method, "requestID", req.ID, "authorizedRequestStr", authorizedRequestStr) return false, "", errors.New("request authorization expired") } - r.lggr.Infow("AuthorizeRequest success in auth", "method", req.Method, "requestID", req.ID, "authorizedRequestStr", authorizedRequestStr) - r.alreadyAuthorizedRequests[authorizedRequestStr] = int64(allowlistedRequest.ExpiryTimestamp) - return true, allowlistedRequest.Owner.Hex(), nil -} -func (r *requestAuthorizer) clearExpiredAuthorizedRequests() { - r.alreadyAuthorizedMutex.Lock() - defer r.alreadyAuthorizedMutex.Unlock() - for request, expiry := range r.alreadyAuthorizedRequests { - if time.Now().UTC().Unix() > expiry { - delete(r.alreadyAuthorizedRequests, request) - } + digestKey := string(allowlistedRequest.RequestDigest[:]) + if err := r.replayGuard.CheckAndRecord(digestKey, int64(allowlistedRequest.ExpiryTimestamp)); err != nil { + r.lggr.Infow("AuthorizeRequest already authorized previously", "method", req.Method, "requestID", req.ID, "authorizedRequestStr", digestKey) + return false, "", err } + + r.lggr.Infow("AuthorizeRequest success in auth", "method", req.Method, "requestID", req.ID, "authorizedRequestStr", digestKey) + return true, allowlistedRequest.Owner.Hex(), nil } func (r *requestAuthorizer) fetchAllowlistedItem(allowListedRequests []workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest, digest [32]byte) *workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest { @@ -99,8 +87,8 @@ func (r *requestAuthorizer) fetchAllowlistedItem(allowListedRequests []workflow_ func NewRequestAuthorizer(lggr logger.Logger, workflowRegistrySyncer workflowsyncerv2.WorkflowRegistrySyncer) *requestAuthorizer { return &requestAuthorizer{ - workflowRegistrySyncer: workflowRegistrySyncer, - lggr: logger.Named(lggr, "VaultRequestAuthorizer"), - alreadyAuthorizedRequests: make(map[string]int64), + workflowRegistrySyncer: workflowRegistrySyncer, + lggr: logger.Named(lggr, "VaultRequestAuthorizer"), + replayGuard: NewDigestReplayGuard(), } } diff --git a/core/capabilities/vault/vaulttypes/types.go b/core/capabilities/vault/vaulttypes/types.go index 31f223fb219..0431a15f7f3 100644 --- a/core/capabilities/vault/vaulttypes/types.go +++ b/core/capabilities/vault/vaulttypes/types.go @@ -5,7 +5,6 @@ import ( "encoding/binary" "encoding/json" "fmt" - "slices" "time" "github.com/ethereum/go-ethereum/common" @@ -15,8 +14,6 @@ import ( "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" vaultcommon "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/vault" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink/v2/core/build" ) var DefaultNamespace = "main" @@ -38,27 +35,12 @@ const ( MaxBatchSize = 10 ) -var ( - // MethodSecretsGet is intentionally omitted from this list, as it is not exposed - // to external clients, but rather used internally by the Workflow DON. - Methods = []string{ - MethodSecretsCreate, - MethodSecretsUpdate, - MethodSecretsDelete, - MethodSecretsList, - MethodPublicKeyGet, - } -) - -func GetSupportedMethods(lggr logger.Logger) []string { - methods := slices.Clone(Methods) - if build.IsDev() { - // Allow secrets get in non-prod environments for testing purposes - // This should never be enabled in production - methods = append(methods, MethodSecretsGet) - lggr.Warnw("enabling vault.secrets.get method since it is not a production build", "build-mode", build.Mode()) - } - return methods +var Methods = []string{ + MethodSecretsCreate, + MethodSecretsUpdate, + MethodSecretsDelete, + MethodSecretsList, + MethodPublicKeyGet, } // SignedOCRResponse is the response format for OCR signed reports, as returned by the Vault DON. diff --git a/core/chainlink.Dockerfile b/core/chainlink.Dockerfile index 879f246ba99..37412ca9077 100644 --- a/core/chainlink.Dockerfile +++ b/core/chainlink.Dockerfile @@ -81,6 +81,10 @@ RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \ RUN if [ ${CHAINLINK_USER} != root ]; then useradd --uid 14933 --create-home ${CHAINLINK_USER}; fi USER ${CHAINLINK_USER} +# Expose image metadata to the running node. +ARG CL_AUTO_DOCKER_TAG=unset +ENV CL_DOCKER_TAG=${CL_AUTO_DOCKER_TAG} + # Set plugin environment variable configuration. ENV CL_SOLANA_CMD=chainlink-solana diff --git a/core/cmd/shell.go b/core/cmd/shell.go index 5ae9f29b360..347bdf22e0b 100644 --- a/core/cmd/shell.go +++ b/core/cmd/shell.go @@ -246,6 +246,12 @@ func (n ChainlinkAppFactory) NewApplication(ctx context.Context, cfg chainlink.G return nil, fmt.Errorf("failed to create workflow fetcher: %w", err) } } + + dockerTag := static.Unset + if envTag, ok := env.DockerTag.Lookup(); ok && envTag != "" { + dockerTag = envTag + } + return chainlink.NewApplication(ctx, chainlink.ApplicationOpts{ Opts: creOpts, Config: cfg, @@ -257,6 +263,7 @@ func (n ChainlinkAppFactory) NewApplication(ctx context.Context, cfg chainlink.G ExternalInitiatorManager: webhook.NewExternalInitiatorManager(ds, unrestrictedClient), Version: static.Version, VersionTag: static.VersionTag, + DockerTag: dockerTag, RestrictedHTTPClient: clhttp.NewRestrictedClient(cfg.Database(), appLggr), UnrestrictedHTTPClient: unrestrictedClient, SecretGenerator: chainlink.FilePersistedSecretGenerator{}, diff --git a/core/config/env/env.go b/core/config/env/env.go index e8be6affca3..5590d2d6681 100644 --- a/core/config/env/env.go +++ b/core/config/env/env.go @@ -10,6 +10,7 @@ import ( var ( Config = Var("CL_CONFIG") + DockerTag = Var("CL_DOCKER_TAG") DatabaseAllowSimplePasswords = Var("CL_DATABASE_ALLOW_SIMPLE_PASSWORDS") IgnorePrereleaseVersionCheck = Var("CL_IGNORE_PRE_RELEASE_VERSION_CHECK") SkipAppVersionCheck = Var("CL_SKIP_APP_VERSION_CHECK") diff --git a/core/internal/testutils/testutils.go b/core/internal/testutils/testutils.go index 5305fcc6c81..7e07bc1741e 100644 --- a/core/internal/testutils/testutils.go +++ b/core/internal/testutils/testutils.go @@ -223,12 +223,3 @@ func AssertCount(t testing.TB, ds sqlutil.DataSource, tableName string, expected func Ptr[T any](v T) *T { return &v } - -func MustRandBytes(n int) (b []byte) { - b = make([]byte, n) - _, err := rand.Read(b) - if err != nil { - panic(err) - } - return -} diff --git a/core/scripts/chaincli/README.md b/core/scripts/chaincli/README.md index bd32c3cbf11..7ca26c58a01 100644 --- a/core/scripts/chaincli/README.md +++ b/core/scripts/chaincli/README.md @@ -22,7 +22,7 @@ to your wallet ahead of executing the next steps Build a local copy of the chainlink docker image by running this command in the root directory of the chainlink repo: ```bash -docker build -t chainlink:local -f ./core/chainlink.Dockerfile . +docker build -t chainlink:local -f ./core/chainlink.Dockerfile --build-arg CL_AUTO_DOCKER_TAG=local . ``` Next, from the root directory again, `cd` into the chaincli directory: @@ -123,4 +123,4 @@ You can use the `grep` and `grepv` flags to filter log lines, e.g. to only show ./chaincli keeper logs --grep keepers-plugin ``` ---- \ No newline at end of file +--- diff --git a/core/scripts/cre/environment/README.md b/core/scripts/cre/environment/README.md index fbb4e31fc8b..e1f4ab68f36 100644 --- a/core/scripts/cre/environment/README.md +++ b/core/scripts/cre/environment/README.md @@ -186,12 +186,9 @@ Builds that access private repositories require `GITHUB_TOKEN` to be set (e.g. ` # while in core/scripts/cre/environment go run . env start [--auto-setup] -# to start environment with an example workflow web API-based workflow +# to start environment with the PoR v2 cron example workflow go run . env start --with-example - # to start environment with an example workflow cron-based workflow (requires cron capability in your image) -go run . env start --with-example --example-workflow-trigger cron - # to start environment with local Beholder go run . env start --with-beholder @@ -209,7 +206,6 @@ Optional parameters: - `-x`: Registers an example PoR workflow using CRE CLI and verifies it executed successfuly - `-s`: Time to wait for example workflow to execute successfuly (defaults to `5m`) - `-p`: **DEPRECATED** Use `image` in TOML config instead. See [Using a pre-built Chainlink image](#using-a-pre-built-chainlink-image). -- `-y`: Trigger for example workflow to deploy (web-trigger or cron). Default: `web-trigger`. **Important!** `cron` trigger requires the Chainlink image to include the cron capability (built from source or a pre-built image with plugins). - `--with-contracts-version`: Version of workflow/capability registries to use (`v2` by default, use `v1` explicitly for legacy coverage) ## Purging environment state @@ -434,12 +430,12 @@ go run . workflow delete-all [flags] go run . workflow delete-all ``` -### `workflow deploy-and-verify-example` -Deploys and verifies the example workflow. +### `workflow run-por-example` +Deploys and verifies the PoR v2 cron example workflow. **Usage:** ```bash -go run . workflow deploy-and-verify-example +go run . workflow run-por-example ``` This command uses default values and is useful for testing the workflow deployment process. @@ -886,58 +882,43 @@ The environment includes several example workflows located in `core/scripts/cre/ - **`v2/node-mode/`**: Node mode workflow example - **`v2/http/`**: HTTP-based workflow example -#### V1 Workflows -- **`v1/proof-of-reserve/cron-based/`**: Cron-based proof-of-reserve workflow -- **`v1/proof-of-reserve/web-trigger-based/`**: Web API trigger-based proof-of-reserve workflow +- **`v2/proof-of-reserve/cron-based/`**: Cron-based proof-of-reserve workflow example ### Deployable Example Workflows -The following workflows can be deployed using the `workflow deploy-and-verify-example` command: +The following workflow can be deployed using the `workflow run-por-example` command: -#### Proof-of-Reserve Workflows -Both proof-of-reserve workflows execute a proof-of-reserve-like scenario with the following steps: +#### Proof-of-Reserve Workflow +The proof-of-reserve workflow executes a proof-of-reserve-like scenario with the following steps: - Call external HTTP API and fetch value of test asset - Reach consensus on that value - Write that value in the consumer contract on chain **Usage:** ```bash -go run . workflow deploy-and-verify-example [flags] +go run . workflow run-por-example [flags] ``` **Key flags:** -- `-y, --example-workflow-trigger`: Trigger type (`web-trigger` or `cron`, default: `web-trigger`) - `-u, --example-workflow-timeout`: Time to wait for workflow execution (default: `5m`) -- `-g, --gateway-url`: Gateway URL for web API trigger (default: `http://localhost:5002`) -- `-d, --don-id`: DON ID for web API trigger (default: `vault`) +- `-d, --workflow-don-id`: Workflow DON ID from the registry (default: `1`) - `-w, --workflow-registry-address`: Workflow registry address (default: `0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0`) - `-r, --rpc-url`: RPC URL (default: `http://localhost:8545`) **Examples:** ```bash -# Deploy cron-based proof-of-reserve workflow -go run . workflow deploy-and-verify-example -y cron +# Deploy the PoR v2 cron example +go run . workflow run-por-example -# Deploy web-trigger-based proof-of-reserve workflow with custom timeout -go run . workflow deploy-and-verify-example -y web-trigger -u 10m +# Deploy the PoR v2 cron example with custom timeout +go run . workflow run-por-example -u 10m ``` #### Cron-based Workflow - **Trigger**: Every 30 seconds on a schedule - **Behavior**: Keeps executing until paused or deleted - **Requirements**: External `cron` capability binary (must be manually compiled or downloaded and configured in TOML) -- **Source**: [`examples/workflows/v1/proof-of-reserve/cron-based/main.go`](./examples/workflows/v1/proof-of-reserve/cron-based/main.go) - -#### Web API Trigger-based Workflow -- **Trigger**: Only when a precisely crafted and cryptographically signed request is made to the gateway node -- **Behavior**: Triggers workflow **once** and only if: - - Sender is whitelisted in the workflow - - Topic is whitelisted in the workflow -- **Source**: [`examples/workflows/v1/proof-of-reserve/web-trigger-based/main.go`](./examples/workflows/v1/proof-of-reserve/web-trigger-based/main.go) - -**Note**: You might see multiple attempts to trigger and verify the workflow when running the example. This is expected and could happen because: -- Topic hasn't been registered yet (nodes haven't downloaded the workflow yet) -- Consensus wasn't reached in time +- **Source**: [`examples/workflows/v2/proof-of-reserve/cron-based/main.go`](./examples/workflows/v2/proof-of-reserve/cron-based/main.go) ### Manual Workflow Deployment @@ -2121,4 +2102,4 @@ Once installed, configure it by running: gh auth login ``` -For GH CLI to be used by the environment to download the CRE CLI you must have access to [smartcontract/dev-platform](https://github.com/smartcontractkit/dev-platform) repository. \ No newline at end of file +For GH CLI to be used by the environment to download the CRE CLI you must have access to [smartcontract/dev-platform](https://github.com/smartcontractkit/dev-platform) repository. diff --git a/core/scripts/cre/environment/completions.go b/core/scripts/cre/environment/completions.go index 47ee879898b..cc8c902f197 100644 --- a/core/scripts/cre/environment/completions.go +++ b/core/scripts/cre/environment/completions.go @@ -244,7 +244,6 @@ func buildCommandTree() *CompletionNode { {Text: "--with-example", Description: "Deploys and registers example workflow (default: false)"}, {Text: "--example-workflow-timeout", Description: "Time to wait until example workflow succeeds (e.g. 10s, 1m, 1h) (default: 5m)"}, {Text: "--with-plugins-docker-image", Description: "Docker image to use (must have all capabilities included)"}, - {Text: "--example-workflow-trigger", Description: "Trigger for example workflow to deploy (web-trigger or cron) (default: web-trigger)"}, {Text: "--with-beholder", Description: "Deploys Beholder (Chip Ingress + Red Panda) (default: false)"}, {Text: "--with-dashboards", Description: "Deploys Observability Stack and Grafana Dashboards (default: false)"}, {Text: "--with-billing", Description: "Deploys Billing Platform Service (default: false)"}, @@ -296,7 +295,7 @@ func buildCommandTree() *CompletionNode { // ENV WORKFLOW - workflow management workflowNode := &CompletionNode{ Suggestions: []prompt.Suggest{ - {Text: "deploy-and-verify-example", Description: "Deploy and verify example workflow"}, + {Text: "run-por-example", Description: "Deploy and verify the PoR v2 cron example workflow"}, {Text: "delete", Description: "Delete a specific workflow"}, {Text: "delete-all", Description: "Delete all workflows"}, {Text: "compile", Description: "Compile a workflow specification"}, diff --git a/core/scripts/cre/environment/environment/environment.go b/core/scripts/cre/environment/environment/environment.go index ce29343e41d..58f11f11353 100644 --- a/core/scripts/cre/environment/environment/environment.go +++ b/core/scripts/cre/environment/environment/environment.go @@ -70,11 +70,6 @@ var ( provisioningStartTime time.Time ) -const ( - WorkflowTriggerWebTrigger = "web-trigger" - WorkflowTriggerCron = "cron" -) - var EnvironmentCmd = &cobra.Command{ Use: "env", Short: "Environment commands", @@ -216,7 +211,6 @@ func startCmd() *cobra.Command { var ( extraAllowedGatewayPorts []int withExampleFlag bool - exampleWorkflowTrigger string exampleWorkflowTimeout time.Duration withPluginsDockerImage string withContractsVersion string @@ -460,9 +454,6 @@ func startCmd() *cobra.Command { return errors.New("no gateway connector configurations found") } - // use first gateway for example workflow - gatewayURL := fmt.Sprintf("%s://%s:%d%s", output.GatewayConnectors.Configurations[0].Incoming.Protocol, output.GatewayConnectors.Configurations[0].Incoming.Host, output.GatewayConnectors.Configurations[0].Incoming.ExternalPort, output.GatewayConnectors.Configurations[0].Incoming.Path) - fmt.Print(libformat.PurpleText("\nRegistering and verifying example workflow\n\n")) workflowRegistryAddress := libcontracts.MustGetAddressFromDataStore(output.CreEnvironment.CldfEnvironment.DataStore, output.CreEnvironment.Blockchains[0].ChainSelector(), keystone_changeset.WorkflowRegistry.String(), output.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()], "") @@ -478,11 +469,7 @@ func startCmd() *cobra.Command { return errors.New("no workflow DON found") } - workflowDON, wErr := output.Dons.OneDonWithFlag(cre.WorkflowDON) - if wErr != nil { - return errors.Wrap(wErr, "failed to get workflow DON") - } - deployErr := deployAndVerifyExampleWorkflow(cmdContext, registryChainOut.CtfOutput().Nodes[0].ExternalHTTPUrl, gatewayURL, workflowDON.Name, workflowDonID, exampleWorkflowTimeout, exampleWorkflowTrigger, workflowRegistryAddress, semver.MustParse(withContractsVersion)) + deployErr := deployAndVerifyExampleWorkflow(cmdContext, registryChainOut.CtfOutput().Nodes[0].ExternalHTTPUrl, workflowDonID, exampleWorkflowTimeout, workflowRegistryAddress, semver.MustParse(withContractsVersion)) if deployErr != nil { fmt.Printf("Failed to deploy and verify example workflow: %s\n", deployErr) } @@ -514,7 +501,6 @@ func startCmd() *cobra.Command { cmd.Flags().BoolVarP(&withExampleFlag, "with-example", "x", false, "Deploys and registers example workflow") cmd.Flags().DurationVarP(&exampleWorkflowTimeout, "example-workflow-timeout", "u", 5*time.Minute, "Time to wait until example workflow succeeds (e.g. 10s, 1m, 1h)") cmd.Flags().StringVarP(&withPluginsDockerImage, "with-plugins-docker-image", "p", "", "DEPRECATED:Docker image to use (set Docker image in TOML config instead)") - cmd.Flags().StringVarP(&exampleWorkflowTrigger, "example-workflow-trigger", "y", "web-trigger", "Trigger for example workflow to deploy (web-trigger or cron)") cmd.Flags().BoolVarP(&withBeholder, "with-beholder", "b", false, "Deploy Beholder (Chip Ingress + Red Panda)") cmd.Flags().BoolVarP(&withDashboards, "with-dashboards", "d", false, "Deploy Observability Stack and Grafana Dashboards") cmd.Flags().BoolVar(&withObs, "with-observability", false, "Start Observability Stack") diff --git a/core/scripts/cre/environment/environment/examples.go b/core/scripts/cre/environment/environment/examples.go index a6fe2e353b6..7a68acc841c 100644 --- a/core/scripts/cre/environment/environment/examples.go +++ b/core/scripts/cre/environment/environment/examples.go @@ -3,9 +3,9 @@ package environment import ( "context" "fmt" + "math/big" "os" "path/filepath" - "strings" "time" "github.com/Masterminds/semver/v3" @@ -14,34 +14,29 @@ import ( "github.com/spf13/cobra" "gopkg.in/yaml.v3" - "github.com/smartcontractkit/chainlink-testing-framework/framework" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/pkg/deploy" - "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/pkg/trigger" "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/pkg/verify" - cronbasedtypes "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/cron-based/types" - webapitriggerbasedtypes "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/types" + portypes "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based/types" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment" creworkflow "github.com/smartcontractkit/chainlink/system-tests/lib/cre/workflow" libformat "github.com/smartcontractkit/chainlink/system-tests/lib/format" + corevm "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" ) func deployAndVerifyExampleWorkflowCmd() *cobra.Command { var ( rpcURLFlag string - gatewayURLFlag string workflowDonIDFlag uint32 - gatewayDonIDFlag string - exampleWorkflowTriggerFlag string exampleWorkflowTimeoutFlag string workflowRegistryAddressFlag string contractsVersionFlag string ) cmd := &cobra.Command{ Use: "run-por-example", - Short: "Runs v1 Proof-of-Reserve example workflow", - Long: `Deploys a simple Proof-of-Reserve workflow and, optionally, wait until it succeeds`, + Short: "Runs the PoR v2 cron example workflow", + Long: `Deploys the PoR v2 cron example workflow and waits until it succeeds`, PersistentPreRun: globalPreRunFunc, RunE: func(cmd *cobra.Command, args []string) error { timeout, timeoutErr := time.ParseDuration(exampleWorkflowTimeoutFlag) @@ -61,13 +56,6 @@ func deployAndVerifyExampleWorkflowCmd() *cobra.Command { } } - gatewayURL := gatewayURLFlag - if !cmd.Flags().Changed("gateway-url") && resolver != nil { - if stateGatewayURL, err := resolver.GatewayURL(); err == nil { - gatewayURL = stateGatewayURL - } - } - workflowDONID := workflowDonIDFlag if !cmd.Flags().Changed("workflow-don-id") && resolver != nil { if stateDONID, err := resolver.WorkflowDONID(); err == nil { @@ -75,75 +63,29 @@ func deployAndVerifyExampleWorkflowCmd() *cobra.Command { } } - gatewayDONName := gatewayDonIDFlag - if !cmd.Flags().Changed("gateway-don-id") && resolver != nil { - if stateDONName, err := resolver.WorkflowDONName(); err == nil { - gatewayDONName = stateDONName - } - } - workflowRegistryAddress, contractsVersion, err := resolveContractAddressAndVersion(cmd, resolver, keystone_changeset.WorkflowRegistry, workflowRegistryAddressFlag, contractsVersionFlag, "workflow-registry-address") if err != nil { return errors.Wrap(err, "❌ failed to resolve workflow registry") } - return deployAndVerifyExampleWorkflow(cmd.Context(), rpcURL, gatewayURL, gatewayDONName, workflowDONID, timeout, exampleWorkflowTriggerFlag, workflowRegistryAddress, contractsVersion) + return deployAndVerifyExampleWorkflow(cmd.Context(), rpcURL, workflowDONID, timeout, workflowRegistryAddress, contractsVersion) }, } cmd.Flags().StringVarP(&rpcURLFlag, "rpc-url", "r", "http://localhost:8545", "RPC URL") - cmd.Flags().StringVarP(&exampleWorkflowTriggerFlag, "example-workflow-trigger", "y", "web-trigger", "Trigger for example workflow to deploy (web-trigger or cron)") cmd.Flags().StringVarP(&exampleWorkflowTimeoutFlag, "example-workflow-timeout", "u", "5m", "Time to wait until example workflow succeeds (e.g. 10s, 1m, 1h)") - cmd.Flags().StringVarP(&gatewayURLFlag, "gateway-url", "g", "http://localhost:5002", "Gateway URL (only for web API trigger-based workflow)") cmd.Flags().Uint32VarP(&workflowDonIDFlag, "workflow-don-id", "d", 1, "DonID used in the workflow registry contract (integer starting with 1)") - cmd.Flags().StringVarP(&gatewayDonIDFlag, "gateway-don-id", "o", "workflow", "Name of the DON that is running web API trigger capability (only for web API trigger-based workflow)") cmd.Flags().StringVarP(&workflowRegistryAddressFlag, "workflow-registry-address", "w", "", "Workflow registry address (if not provided, address from the state file will be used)") - cmd.Flags().StringVar(&contractsVersionFlag, "with-contracts-version", "", "Version of workflow registry contract to use (v1 or v2)") + cmd.Flags().StringVar(&contractsVersionFlag, "with-contracts-version", "v2", "Version of workflow registry contract to use (v1 or v2)") return cmd } -type executableWorkflowFn = func(cmdContext context.Context, rpcURL, gatewayURL, gatewayDonID, privateKey string, consumerContractAddress common.Address, feedID string, waitTime time.Duration, startTime time.Time) error - -func executeWebTriggerBasedWorkflow(cmdContext context.Context, rpcURL, gatewayURL, gatewayDonID, privateKey string, consumerContractAddress common.Address, feedID string, waitTime time.Duration, startTime time.Time) error { - ticker := 5 * time.Second - for { - select { - case <-time.After(waitTime): - fmt.Print(libformat.PurpleText("\n[Stage 3/3] Example workflow failed to execute successfully in %.2f seconds\n", time.Since(startTime).Seconds())) - - return fmt.Errorf("example workflow failed to execute successfully within %s", waitTime) - case <-time.Tick(ticker): - triggerErr := trigger.WebAPITriggerValue( - gatewayURL, - gatewayDonID, - privateKey, - 5*time.Minute, - ) - if triggerErr == nil { - verifyTime := 25 * time.Second - verifyErr := verify.ProofOfReserve(rpcURL, consumerContractAddress.Hex(), feedID, true, verifyTime) - if verifyErr == nil { - if isBlockscoutRunning(cmdContext) { - fmt.Print(libformat.PurpleText("Open http://localhost/address/%s?tab=internal_txns to check consumer contract's transaction history\n", consumerContractAddress.Hex())) - } - - return nil - } - - fmt.Printf("\nTrying to verify workflow again in %.2f seconds...\n\n", ticker.Seconds()) - } else { - framework.L.Debug().Msgf("failed to trigger web API trigger: %s", triggerErr) - } - } - } -} - -func executeCronBasedWorkflow(cmdContext context.Context, rpcURL, _, _, privateKey string, consumerContractAddress common.Address, feedID string, waitTime time.Duration, startTime time.Time) error { +func executeCronBasedWorkflow(cmdContext context.Context, rpcURL string, consumerContractAddress common.Address, feedID string, waitTime time.Duration, startTime time.Time) error { // we ignore return as if verification failed it will print that info verifyErr := verify.ProofOfReserve(rpcURL, consumerContractAddress.Hex(), feedID, true, waitTime) if verifyErr != nil { - fmt.Print(libformat.PurpleText("\n[Stage 3/3] Example workflow failed to execute successfully in %.2f seconds\n", time.Since(startTime).Seconds())) + fmt.Print(libformat.PurpleText("\n[Stage 4/4] Example workflow failed to execute successfully in %.2f seconds\n", time.Since(startTime).Seconds())) return errors.Wrap(verifyErr, "failed to verify example workflow") } @@ -154,7 +96,7 @@ func executeCronBasedWorkflow(cmdContext context.Context, rpcURL, _, _, privateK return nil } -func deployAndVerifyExampleWorkflow(cmdContext context.Context, rpcURL, gatewayURL, gatewayDonID string, workflowDonID uint32, timeout time.Duration, exampleWorkflowTrigger, workflowRegistryAddress string, contractsVersion *semver.Version) error { +func deployAndVerifyExampleWorkflow(cmdContext context.Context, rpcURL string, workflowDonID uint32, timeout time.Duration, workflowRegistryAddress string, contractsVersion *semver.Version) error { totalStart := time.Now() start := time.Now() @@ -179,32 +121,32 @@ func deployAndVerifyExampleWorkflow(cmdContext context.Context, rpcURL, gatewayU fmt.Print(libformat.PurpleText("\n[Stage 2/4] Deployed Balance Reader in %.2f seconds\n", time.Since(start).Seconds())) start = time.Now() - fmt.Print(libformat.PurpleText("[Stage 3/4] Registering example Proof-of-Reserve workflow\n\n")) + fmt.Print(libformat.PurpleText("[Stage 3/4] Registering PoR v2 cron example workflow\n\n")) - var executableWorkflowFunction executableWorkflowFn - - var workflowName string - var workflowFilePath string - var configFilePath string - var configErr error + workflowName := "por-v2-cron-example" + workflowFilePath := "examples/workflows/v2/proof-of-reserve/cron-based/main.go" feedID := "0x018e16c39e0003200000000000000000" + chainID, chainSelector, chainErr := deploy.ChainMetadata(rpcURL) + if chainErr != nil { + return errors.Wrap(chainErr, "failed to resolve chain metadata for PoR config") + } - if strings.EqualFold(exampleWorkflowTrigger, WorkflowTriggerCron) { - workflowName = "cron-based-proof-of-reserve" - workflowFilePath = "examples/workflows/v1/proof-of-reserve/cron-based/main.go" - configFilePath, configErr = builAndSavePoRCronConfig(consumerContractAddress.Hex(), balanceReaderContractAddress.Hex(), feedID, filepath.Dir(workflowFilePath)) - if configErr != nil { - return errors.Wrap(configErr, "failed to build and save PoR config") - } - executableWorkflowFunction = executeCronBasedWorkflow - } else { - workflowName = "web-trigger-based-proof-of-reserve" - workflowFilePath = "examples/workflows/v1/proof-of-reserve/web-trigger-based/main.go" - configFilePath, configErr = builAndSavePoRWebTriggerConfig(consumerContractAddress.Hex(), balanceReaderContractAddress.Hex(), feedID, filepath.Dir(workflowFilePath)) - if configErr != nil { - return errors.Wrap(configErr, "failed to build and save PoR config") - } - executableWorkflowFunction = executeWebTriggerBasedWorkflow + addressesToRead, addressesErr := deploy.CreateAndFundAddresses(rpcURL, 2, big.NewInt(10)) + if addressesErr != nil { + return errors.Wrap(addressesErr, "failed to create and fund addresses for PoR config") + } + + configFilePath, configErr := buildAndSavePoRV2CronConfig( + consumerContractAddress.Hex(), + balanceReaderContractAddress.Hex(), + feedID, + chainSelector, + corevm.GenerateWriteTargetName(chainID), + addressesToRead, + filepath.Dir(workflowFilePath), + ) + if configErr != nil { + return errors.Wrap(configErr, "failed to build and save PoR config") } defer func() { @@ -241,54 +183,29 @@ func deployAndVerifyExampleWorkflow(cmdContext context.Context, rpcURL, gatewayU return pkErr } - return executableWorkflowFunction(cmdContext, rpcURL, gatewayURL, gatewayDonID, os.Getenv("PRIVATE_KEY"), *consumerContractAddress, feedID, timeout, totalStart) + return executeCronBasedWorkflow(cmdContext, rpcURL, *consumerContractAddress, feedID, timeout, totalStart) } -func builAndSavePoRWebTriggerConfig(dataFeedsCacheAddress, balanceReaderAddress, feedID, folder string) (string, error) { - cfg := webapitriggerbasedtypes.WorkflowConfig{ - DataFeedsCacheAddress: dataFeedsCacheAddress, - AllowedTriggerSender: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - AllowedTriggerTopic: "sendValue", - FeedID: feedID, - WriteTargetName: "write_geth-testnet@1.0.0", - ChainFamily: "evm", - ChainID: "1337", - BalanceReaderConfig: webapitriggerbasedtypes.BalanceReaderConfig{ - BalanceReaderAddress: balanceReaderAddress, - }, - } - - yaml, yamlErr := yaml.Marshal(cfg) - if yamlErr != nil { - return "", errors.Wrap(yamlErr, "failed to marshal config to YAML") - } - - filePath := filepath.Join(folder, "web_trigger_config.yaml") - writeErr := os.WriteFile(filePath, yaml, 0644) //nolint:gosec // G306: we want it to be readable by everyone - if writeErr != nil { - return "", errors.Wrap(writeErr, "failed to write config to file") - } - - return filePath, nil -} - -func builAndSavePoRCronConfig(dataFeedsCacheAddress, balanceReaderAddress, feedID, folder string) (string, error) { +func buildAndSavePoRV2CronConfig(dataFeedsCacheAddress, balanceReaderAddress, feedID string, chainSelector uint64, writeTargetName string, addressesToRead []common.Address, folder string) (string, error) { if feedID == "" { return "", errors.New("feedID is empty") } + if len(addressesToRead) < 2 { + return "", errors.New("at least two addresses are required for the PoR v2 example") + } - cfg := cronbasedtypes.WorkflowConfig{ - ComputeConfig: cronbasedtypes.ComputeConfig{ + cfg := portypes.WorkflowConfig{ + ChainSelector: chainSelector, + ComputeConfig: portypes.ComputeConfig{ DataFeedsCacheAddress: dataFeedsCacheAddress, URL: "https://api.real-time-reserves.verinumus.io/v1/chainlink/proof-of-reserves/TrueUSD", FeedID: feedID, - WriteTargetName: "write_geth-testnet@1.0.0", + WriteTargetName: writeTargetName, }, - BalanceReaderConfig: cronbasedtypes.BalanceReaderConfig{ + BalanceReaderConfig: portypes.BalanceReaderConfig{ BalanceReaderAddress: balanceReaderAddress, + AddressesToRead: addressesToRead, }, - ChainFamily: "evm", - ChainID: "1337", } yaml, yamlErr := yaml.Marshal(cfg) @@ -296,7 +213,7 @@ func builAndSavePoRCronConfig(dataFeedsCacheAddress, balanceReaderAddress, feedI return "", errors.Wrap(yamlErr, "failed to marshal config to YAML") } - filePath := filepath.Join(folder, "cron_config.yaml") + filePath := filepath.Join(folder, "config.yaml") writeErr := os.WriteFile(filePath, yaml, 0644) //nolint:gosec // G306: we want it to be readable by everyone if writeErr != nil { return "", errors.Wrap(writeErr, "failed to write config to file") diff --git a/core/scripts/cre/environment/environment/state_resolver_test.go b/core/scripts/cre/environment/environment/state_resolver_test.go deleted file mode 100644 index cf6e2ee3a20..00000000000 --- a/core/scripts/cre/environment/environment/state_resolver_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package environment - -import ( - "os" - "testing" - - "github.com/Masterminds/semver/v3" - "github.com/spf13/cobra" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" - "github.com/smartcontractkit/chainlink/system-tests/lib/cre" - envconfig "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/config" - "github.com/smartcontractkit/chainlink/system-tests/lib/infra" -) - -func TestResolverAddressRef(t *testing.T) { - t.Parallel() - - cfg := &envconfig.Config{} - require.NoError(t, cfg.SetAddresses([]datastore.AddressRef{{ - Address: "0x123", - Type: datastore.ContractType(keystone_changeset.WorkflowRegistry), - ChainSelector: 1337, - Version: semver.MustParse("2.0.0"), - }})) - - resolver := &LocalCREStateResolver{cfg: cfg} - - addrRef, err := resolver.AddressRef(keystone_changeset.WorkflowRegistry) - require.NoError(t, err) - require.Equal(t, "0x123", addrRef.Address) - require.Equal(t, uint64(1337), addrRef.ChainSelector) - require.Equal(t, semver.MustParse("2.0.0").String(), addrRef.Version.String()) -} - -func TestResolverWorkflowDONMetadata(t *testing.T) { - t.Parallel() - - donsMetadata, err := cre.NewDonsMetadata([]*cre.DonMetadata{{ - Name: "workflow-don", - ID: 7, - Flags: []string{cre.WorkflowDON}, - NodesMetadata: []*cre.NodeMetadata{{ - Roles: []string{cre.BootstrapNode}, - }}, - }}, infra.Provider{Type: infra.Docker}) - require.NoError(t, err) - - resolver := &LocalCREStateResolver{ - topology: &cre.Topology{DonsMetadata: donsMetadata}, - } - - workflowDON, err := resolver.WorkflowDONMetadata() - require.NoError(t, err) - require.Equal(t, "workflow-don", workflowDON.Name) - - workflowDONID, err := resolver.WorkflowDONID() - require.NoError(t, err) - require.Equal(t, uint32(7), workflowDONID) - - workflowDONName, err := resolver.WorkflowDONName() - require.NoError(t, err) - require.Equal(t, "workflow-don", workflowDONName) -} - -func TestResolverGatewayURLFallsBackToProviderHost(t *testing.T) { - t.Parallel() - - resolver := &LocalCREStateResolver{ - cfg: &envconfig.Config{ - Infra: &infra.Provider{Type: infra.Docker}, - }, - topology: &cre.Topology{ - GatewayConnectors: &cre.GatewayConnectors{ - Configurations: []*cre.DonGatewayConfiguration{{ - GatewayConfiguration: &cre.GatewayConfiguration{ - Incoming: cre.Incoming{ - Protocol: "http", - ExternalPort: 5002, - Path: "/", - }, - }, - }}, - }, - }, - } - - gatewayURL, err := resolver.GatewayURL() - require.NoError(t, err) - require.Equal(t, "http://localhost:5002/", gatewayURL) -} - -func TestResolveContractAddressAndVersion(t *testing.T) { - t.Parallel() - - makeCmd := func(address string) *cobra.Command { - cmd := newCobraCommand() - cmd.Flags().String("workflow-registry-address", "", "") - require.NoError(t, cmd.Flags().Set("workflow-registry-address", address)) - return cmd - } - - t.Run("uses state when flag not changed", func(t *testing.T) { - cfg := &envconfig.Config{} - require.NoError(t, cfg.SetAddresses([]datastore.AddressRef{{ - Address: "0x456", - Type: datastore.ContractType(keystone_changeset.WorkflowRegistry), - Version: semver.MustParse("1.1.0"), - }})) - - cmd := newCobraCommand() - cmd.Flags().String("workflow-registry-address", "", "") - - address, version, err := resolveContractAddressAndVersion(cmd, &LocalCREStateResolver{cfg: cfg}, keystone_changeset.WorkflowRegistry, "", "2.0.0", "workflow-registry-address") - require.NoError(t, err) - require.Equal(t, "0x456", address) - require.Equal(t, "1.1.0", version.String()) - }) - - t.Run("uses explicit override when flag changed", func(t *testing.T) { - cmd := makeCmd("0xabc") - - address, version, err := resolveContractAddressAndVersion(cmd, nil, keystone_changeset.WorkflowRegistry, "0xabc", "2.0.0", "workflow-registry-address") - require.NoError(t, err) - require.Equal(t, "0xabc", address) - require.Equal(t, "2.0.0", version.String()) - }) -} - -func TestToDockerHostRPC(t *testing.T) { - t.Parallel() - - expected := "http://host.docker.internal:8545" - if os.Getenv("CI") == "true" { - expected = "http://172.17.0.1:8545" - } - - require.Equal(t, expected, toDockerHostRPC("http://localhost:8545")) - require.Equal(t, expected, toDockerHostRPC("http://127.0.0.1:8545")) -} - -func newCobraCommand() *cobra.Command { - return &cobra.Command{Use: "test"} -} diff --git a/core/scripts/cre/environment/examples/pkg/deploy/consumer.go b/core/scripts/cre/environment/examples/pkg/deploy/consumer.go index d345f675af9..3e9a2943503 100644 --- a/core/scripts/cre/environment/examples/pkg/deploy/consumer.go +++ b/core/scripts/cre/environment/examples/pkg/deploy/consumer.go @@ -1,18 +1,24 @@ package deploy import ( + "context" + "math/big" "os" "time" "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" + chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-testing-framework/framework" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/smartcontractkit/chainlink-testing-framework/seth" "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/balance_reader" "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/contracts/permissionless_feeds_consumer" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment" + crecrypto "github.com/smartcontractkit/chainlink/system-tests/lib/crypto" + libfunding "github.com/smartcontractkit/chainlink/system-tests/lib/funding" ) func PermissionlessFeedsConsumer(rpcURL string) (*common.Address, error) { @@ -70,3 +76,70 @@ func BalanceReader(rpcURL string) (*common.Address, error) { return &data.Address, nil } + +func ChainMetadata(rpcURL string) (uint64, uint64, error) { + sethClient, err := newSethClient(rpcURL) + if err != nil { + return 0, 0, err + } + + chainID := sethClient.Cfg.Network.ChainID + chainSelector, err := chainselectors.SelectorFromChainId(chainID) + if err != nil { + return 0, 0, errors.Wrapf(err, "failed to resolve chain selector for chain id %d", chainID) + } + + return chainID, chainSelector, nil +} + +func CreateAndFundAddresses(rpcURL string, count int, amount *big.Int) ([]common.Address, error) { + if count <= 0 { + return nil, errors.New("count must be greater than zero") + } + if amount == nil { + return nil, errors.New("amount is nil") + } + + sethClient, err := newSethClient(rpcURL) + if err != nil { + return nil, err + } + + addresses := make([]common.Address, 0, count) + for range count { + address, _, err := crecrypto.GenerateNewKeyPair() + if err != nil { + return nil, errors.Wrap(err, "failed to generate address") + } + + _, err = libfunding.SendFunds(context.Background(), framework.L, sethClient, libfunding.FundsToSend{ + ToAddress: address, + Amount: new(big.Int).Set(amount), + PrivateKey: sethClient.MustGetRootPrivateKey(), + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to fund address %s", address.Hex()) + } + + addresses = append(addresses, address) + } + + return addresses, nil +} + +func newSethClient(rpcURL string) (*seth.Client, error) { + if pkErr := environment.SetDefaultPrivateKeyIfEmpty(blockchain.DefaultAnvilPrivateKey); pkErr != nil { + return nil, pkErr + } + + sethClient, sethErr := seth.NewClientBuilder(). + WithRpcUrl(rpcURL). + WithPrivateKeys([]string{os.Getenv("PRIVATE_KEY")}). + WithProtections(false, false, seth.MustMakeDuration(time.Second)). + Build() + if sethErr != nil { + return nil, errors.Wrap(sethErr, "failed to create Seth Ethereum client") + } + + return sethClient, nil +} diff --git a/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/go.mod b/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/go.mod deleted file mode 100644 index 7ea8adc9e3a..00000000000 --- a/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/go.mod +++ /dev/null @@ -1,91 +0,0 @@ -module github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based - -go 1.25.7 - -require ( - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317233127-178dd2eeaa87 - github.com/smartcontractkit/chainlink/v2 v2.32.0 - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 // indirect - github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.28.0 // indirect - github.com/go-viper/mapstructure/v2 v2.5.0 // indirect - github.com/goccy/go-yaml v1.12.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect - github.com/iancoleman/strcase v0.3.0 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/sanity-io/litter v1.5.5 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect - github.com/shopspring/decimal v1.4.0 // indirect - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 // indirect - github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect - go.opentelemetry.io/otel/log v0.15.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/sdk v1.41.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.15.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect - go.opentelemetry.io/proto/otlp v1.9.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.1 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.50.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.42.0 // indirect - golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect - google.golang.org/grpc v1.78.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect -) diff --git a/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/go.sum b/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/go.sum deleted file mode 100644 index 4b933398b36..00000000000 --- a/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/go.sum +++ /dev/null @@ -1,213 +0,0 @@ -github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c h1:cxQVoh6kY+c4b0HUchHjGWBI8288VhH50qxKG3hdEg0= -github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c/go.mod h1:3XzxudkrYVUvbduN/uI2fl4lSrMSzU0+3RCu2mpnfx8= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 h1:ydUjnKn4RoCeN8rge3F/deT52w2WJMmIC5mHNUq+Ut8= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2/go.mod h1:Bny999RuVUtNjzTGa9HCHpXjrLGMipJVq5kqVpudBl0= -github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= -github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= -github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= -github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= -github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= -github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= -github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= -github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= -github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317233127-178dd2eeaa87 h1:nvv1kiv/7jwALkFztO//NhIq4Y9M4kmJ0UCgTZMC/qI= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317233127-178dd2eeaa87/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10/go.mod h1:oiDa54M0FwxevWwyAX773lwdWvFYYlYHHQV1LQ5HpWY= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 h1:03tbcwjyIEjvHba1IWOj1sfThwebm2XNzyFHSuZtlWc= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= -github.com/smartcontractkit/chainlink/v2 v2.32.0 h1:Fax3JOIAa0uvthgLsd34ktekmSkrDMP2rl0/KFVugcY= -github.com/smartcontractkit/chainlink/v2 v2.32.0/go.mod h1:MIh2RAuTXdC3voDTo5+AtPyJPQfeIH5hkBDZQ0P1tjg= -github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e h1:poXTj5cFVM6XfC4HICIDYkDVc/A6OYB0eeID0wU2JQE= -github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 h1:tPLwQlXbJ8NSOfZc4OkgU5h2A38M4c9kfHSVc4PFQGs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2/go.mod h1:QTnxBwT/1rBIgAG1goq6xMydfYOBKU6KTiYF4fp5zL8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 h1:gAU726w9J8fwr4qRDqu1GYMNNs4gXrU+Pv20/N1UpB4= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 h1:yEX3aC9KDgvYPhuKECHbOlr5GLwH6KTjLJ1sBSkkxkc= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0/go.mod h1:/GXR0tBmmkxDaCUGahvksvp66mx4yh5+cFXgSlhg0vQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= -go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= -go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= -go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= -go.opentelemetry.io/otel/sdk/log/logtest v0.13.0 h1:9yio6AFZ3QD9j9oqshV1Ibm9gPLlHNxurno5BreMtIA= -go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= -golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 h1:X9z6obt+cWRX8XjDVOn+SZWhWe5kZHm46TThU9j+jss= -google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/main.go b/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/main.go deleted file mode 100644 index 6a934b44567..00000000000 --- a/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/main.go +++ /dev/null @@ -1,171 +0,0 @@ -package main - -import ( - "encoding/hex" - "fmt" - "math/big" - "time" - - "gopkg.in/yaml.v3" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/aggregators" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/ocr3cap" - "github.com/smartcontractkit/chainlink-common/pkg/capabilities/targets/chainwriter" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" - "github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm" - types "github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/types" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/webapi/webapicap" -) - -func main() { - runner := wasm.NewRunner() - workflow, err := valueTriggeredWorkflow(runner) - if err != nil { - panic(err) - } - runner.Run(workflow) -} - -func valueTriggeredWorkflow(r sdk.Runner) (*sdk.WorkflowSpecFactory, error) { - var workflowConfig types.WorkflowConfig - if err := yaml.Unmarshal(r.Config(), &workflowConfig); err != nil { - return nil, err - } - - w := sdk.NewWorkflowSpecFactory() - - // Web API Trigger: define allowed sender, rate limits, required parameters - trigger := webapicap.TriggerConfig{ - AllowedSenders: []string{workflowConfig.AllowedTriggerSender}, - AllowedTopics: []string{workflowConfig.AllowedTriggerTopic}, - RateLimiter: webapicap.RateLimiterConfig{ - GlobalBurst: 1000, - GlobalRPS: 1000, - PerSenderBurst: 1000, - PerSenderRPS: 1000, - }, - RequiredParams: []string{"value"}, - }.New(w) - - // Compute: get value from event - contractValue := sdk.Compute1( - w, - "getValue", - sdk.Compute1Inputs[webapicap.TriggerRequestPayloadParams]{Arg0: trigger.Params().(sdk.CapDefinition[webapicap.TriggerRequestPayloadParams])}, - func(SDK sdk.Runtime, o webapicap.TriggerRequestPayloadParams) (ValueOutput, error) { - if len(o) == 0 { - return ValueOutput{}, fmt.Errorf("no data found in event") - } - - maybeValue, ok := o["value"] - if !ok { - return ValueOutput{}, fmt.Errorf("value with name 'value' not found in payload") - } - - valueStr, ok := maybeValue.(string) - if !ok { - return ValueOutput{}, fmt.Errorf("value is not a string, but %T", maybeValue) - } - - valueBigInt := new(big.Int) - valueBigInt, ok = valueBigInt.SetString(valueStr, 10) - if !ok { - return ValueOutput{}, fmt.Errorf("failed to convert value %s to big.Int", valueStr) - } - - // Convert the FeedID string to a byte array - feedIDBytes, err := convertFeedIDtoBytes(workflowConfig.FeedID) - if err != nil { - return ValueOutput{}, fmt.Errorf("failed to convert FeedID to bytes: %w", err) - } - - return ValueOutput{ - Price: valueBigInt, - Timestamp: time.Now().Unix(), - FeedID: feedIDBytes, - }, nil - }, - ) - - // Consensus: all observations are aggregated, timestamps can be maximum 30 seconds apart - // median of all values is used as the value price - consensusInput := ocr3cap.ReduceConsensusInput[ValueOutput]{ - Observation: contractValue.Value(), - } - - consensus := ocr3cap.ReduceConsensusConfig[ValueOutput]{ - Encoder: ocr3cap.EncoderEVM, - EncoderConfig: map[string]any{ - "abi": "(bytes32 FeedID, uint32 Timestamp, uint224 Price)[] Reports", - }, - ReportID: "0001", - KeyID: "evm", - AggregationConfig: aggregators.ReduceAggConfig{ - Fields: []aggregators.AggregationField{ - { - InputKey: "FeedID", - OutputKey: "FeedID", - Method: "mode", - }, - { - InputKey: "Price", - OutputKey: "Price", - Method: "median", - DeviationType: "any", - }, - { - InputKey: "Timestamp", - OutputKey: "Timestamp", - Method: "median", - DeviationString: "30", - DeviationType: "absolute", - }, - }, - ReportFormat: aggregators.REPORT_FORMAT_ARRAY, - }, - }.New(w, "consensus", consensusInput) - - // Write: write the median price to the Data Feeds Cache contract - targetInput := chainwriter.TargetInput{ - SignedReport: consensus, - } - - writeTargetName := "write_geth-testnet@1.0.0" - if workflowConfig.WriteTargetName != "" { - writeTargetName = workflowConfig.WriteTargetName - } - - chainwriter.TargetConfig{ - CreStepTimeout: 40, // 10 seconds - Address: workflowConfig.DataFeedsCacheAddress, // Data Feeds Cache contract address - DeltaStage: "15s", - Schedule: "oneAtATime", - }.New(w, writeTargetName, targetInput) - - return w, nil -} - -func Ptr[T any](v T) *T { - return &v -} - -func convertFeedIDtoBytes(feedIDStr string) ([32]byte, error) { - b, err := hex.DecodeString(feedIDStr[2:]) - if err != nil { - return [32]byte{}, err - } - - if len(b) < 32 { - nb := [32]byte{} - copy(nb[:], b[:]) - return nb, err - } - - return [32]byte(b), nil -} - -type ValueOutput struct { - Price *big.Int - Timestamp int64 - FeedID [32]byte -} diff --git a/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/types/types.go b/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/types/types.go deleted file mode 100644 index bddb2a4d49e..00000000000 --- a/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based/types/types.go +++ /dev/null @@ -1,16 +0,0 @@ -package types - -type WorkflowConfig struct { - WriteTargetName string `yaml:"write_target_name"` - ChainFamily string `yaml:"chain_family,omitempty"` - ChainID string `yaml:"chain_id,omitempty"` - DataFeedsCacheAddress string `yaml:"data_feeds_cache_address"` - AllowedTriggerSender string `yaml:"allowed_trigger_sender"` - AllowedTriggerTopic string `yaml:"allowed_trigger_topic"` - FeedID string `yaml:"feed_id"` - BalanceReaderConfig -} - -type BalanceReaderConfig struct { - BalanceReaderAddress string `yaml:"balance_reader_address"` -} diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 91ac620e082..a66700febe8 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -9,9 +9,7 @@ replace github.com/smartcontractkit/chainlink/deployment => ../../deployment replace github.com/smartcontractkit/chainlink/system-tests/lib => ../../system-tests/lib -replace github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/cron-based => ./cre/environment/examples/workflows/v1/proof-of-reserve/cron-based - -replace github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based => ./cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based +replace github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based => ./cre/environment/examples/workflows/v2/proof-of-reserve/cron-based // Using a separate `require` here to avoid surrounding line changes // creating potential merge conflicts. @@ -44,22 +42,22 @@ require ( github.com/prometheus/client_golang v1.23.2 github.com/rs/zerolog v1.34.0 github.com/shopspring/decimal v1.4.0 + github.com/smartcontractkit/chain-selectors v1.0.97 github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 - github.com/smartcontractkit/chainlink-common v0.11.0 + github.com/smartcontractkit/chainlink-common v0.11.1 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-data-streams v0.1.13 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260320152158-2191d797b5ce github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.5 github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.20 github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.5 github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 - github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/cron-based v0.0.0-20251020210257-0a6ec41648b4 - github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based v0.0.0-20251020210257-0a6ec41648b4 + github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/chainlink/system-tests/lib v0.0.0-20251020210257-0a6ec41648b4 github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.0 github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e @@ -485,7 +483,6 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.1.0 // indirect - github.com/smartcontractkit/chain-selectors v1.0.97 // indirect github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 // indirect github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260323224438-d819cb3228e1 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 // indirect @@ -505,14 +502,14 @@ require ( github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706 // indirect github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 // indirect github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3 // indirect github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f // indirect - github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 // indirect + github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c // indirect github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075 // indirect github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 // indirect github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index ed50a03a057..53e09f270d9 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1634,8 +1634,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7db87ae70/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.0 h1:b/6fGMruUCKqxxzNBmTjCupRkd+m6LqvPCBBMTkpxU0= -github.com/smartcontractkit/chainlink-common v0.11.0/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.11.1 h1:JVTnqoQjdLDmQQXNgssmzEQnJK0gQ/0427LqS4UDuqE= +github.com/smartcontractkit/chainlink-common v0.11.1/go.mod h1:9W8E7tfchAsrSNHdMM1mzLmle+bL1P8Ou0I4LG1qNxw= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 h1:NOUsjsMzNecbjiPWUQGlRSRAutEvCFrqqyETDJeh5q4= @@ -1672,14 +1672,14 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0. github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:ATjAPIVJibHRcIfiG47rEQkUIOoYa6KDvWj3zwCAw6g= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d h1:AJy55QJ/pBhXkZjc7N+ATnWfxrcjq9BI9DmdtdjwDUQ= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 h1:03tbcwjyIEjvHba1IWOj1sfThwebm2XNzyFHSuZtlWc= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 h1:fkS5FJpSozwxL2FA6OJDi7az2DrtMNiK1X5DWuHDyfA= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 h1:hhevsu8k7tlDRrYZmgAh7V4avGQDMvus1bwIlial3Ps= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 h1:oli+2uLU6jcrJGCuYFqk3475hiwL17SWlITWLv+tx/w= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 h1:AEnxv4HM3WD1RbQkRiFyb9cJ6YKAcqBp1CpIcFdZfuo= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= @@ -1694,8 +1694,8 @@ github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f h1:3+vQMwuWL6+OqNutFqo/+gkczJwcr+MBPqeSxcjfI1Y= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 h1:XLMJ6FDQoEiqDNZ4B1MV9Vi1lL8vOfo9SzgqkM8IiuA= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c h1:7MUil5RQBxxnmfwp2bc1N4jv/8FVLH0hAkJupnGNMCg= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075 h1:PcR7Zdh+Z+Dh/S4lQ1xDbnFrb6He70KW9O5+9DtgloE= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075/go.mod h1:APCV5fIW/a+JGM+Cz9yb6XyGt8ht5hISEYfpG/k4Z+k= github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1:4mGJySR1GAJAAFRwEo6YiSKM2zSHzYT5b/FSmrpNUGI= @@ -1817,6 +1817,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/suzuki-shunsuke/go-convmap v0.2.1 h1:g94CxI6ENYluXZhdEH+1WVGhMAE8nLvAmWLUCwBw6W0= +github.com/suzuki-shunsuke/go-convmap v0.2.1/go.mod h1:3XfGRbtyNBMGfXAxhROSRki6/UIlUX31Qt6DvdI6lUs= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index 9ee3b2bc8c9..4803a14faa1 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -202,6 +202,7 @@ type ApplicationOpts struct { ExternalInitiatorManager webhook.ExternalInitiatorManager Version string VersionTag string + DockerTag string RestrictedHTTPClient *http.Client UnrestrictedHTTPClient *http.Client SecretGenerator SecretGenerator @@ -223,7 +224,8 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err var srvcs []services.ServiceCtx heartbeat := NewHeartbeat(NewHeartbeatConfig(opts)) - srvcs = append(srvcs, &heartbeat) + nodePlatformBuildInfo := NewNodePlatformBuildInfoService(NewNodePlatformBuildInfoConfig(opts)) + srvcs = append(srvcs, &heartbeat, &nodePlatformBuildInfo) auditLogger := opts.AuditLogger cfg := opts.Config diff --git a/core/services/chainlink/node_platform.go b/core/services/chainlink/node_platform.go new file mode 100644 index 00000000000..b96590b0972 --- /dev/null +++ b/core/services/chainlink/node_platform.go @@ -0,0 +1,136 @@ +package chainlink + +import ( + "context" + "time" + + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + commonservices "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/timeutil" + commonv1 "github.com/smartcontractkit/chainlink-protos/node-platform/common/v1" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/static" +) + +const ( + nodePlatformDomain = "node-platform" + nodePlatformEntity = "common.v1.NodeBuildInfo" + nodePlatformDataSchema = "/node-platform/common/v1" +) + +type NodePlatformBuildInfoService struct { + commonservices.Service + eng *commonservices.Engine + + opts NodePlatformBuildInfoConfig + beat time.Duration + emitter beholder.Emitter +} + +type NodePlatformBuildInfoConfig struct { + Beat time.Duration + Lggr logger.Logger + CSAKeyStore keystore.CSA + CSAPublicKey string + CommitSHA string + DockerTag string + VersionTag string + Version string +} + +func NewNodePlatformBuildInfoConfig(opts ApplicationOpts) NodePlatformBuildInfoConfig { + version := opts.Version + if version == "" { + version = static.Version + } + + versionTag := opts.VersionTag + if versionTag == "" { + versionTag = static.VersionTag + } + + dockerTag := opts.DockerTag + if dockerTag == "" { + dockerTag = static.Unset + } + + return NodePlatformBuildInfoConfig{ + Beat: opts.Config.Telemetry().HeartbeatInterval(), + Lggr: opts.Logger, + CSAKeyStore: opts.KeyStore.CSA(), + CommitSHA: static.Sha, + DockerTag: dockerTag, + VersionTag: versionTag, + Version: version, + } +} + +func NewNodePlatformBuildInfoService(cfg NodePlatformBuildInfoConfig) NodePlatformBuildInfoService { + s := NodePlatformBuildInfoService{ + opts: cfg, + beat: cfg.Beat, + emitter: beholder.GetEmitter(), + } + + s.Service, s.eng = commonservices.Config{ + Name: "NodePlatformBuildInfo", + Start: s.start, + }.NewServiceEngine(cfg.Lggr) + + return s +} + +func (s *NodePlatformBuildInfoService) start(ctx context.Context) error { + s.resolveCSAPublicKey(ctx) + s.eng.GoTick(timeutil.NewTicker(s.GetBeat), s.emit) + return nil +} + +func (s *NodePlatformBuildInfoService) resolveCSAPublicKey(ctx context.Context) { + if s.opts.CSAKeyStore == nil { + return + } + + csaKey, err := keystore.GetDefault(ctx, s.opts.CSAKeyStore) + if err != nil { + s.eng.Errorw("failed to resolve CSA key for node-platform build info", "err", err) + return + } + + s.opts.CSAPublicKey = csaKey.PublicKeyString() +} + +func (s *NodePlatformBuildInfoService) emit(ctx context.Context) { + payloadBytes, err := proto.Marshal(&commonv1.NodeBuildInfo{ + CsaPublicKey: s.opts.CSAPublicKey, + CommitSha: s.opts.CommitSHA, + DockerTag: s.opts.DockerTag, + VersionTag: s.opts.VersionTag, + Version: s.opts.Version, + }) + if err != nil { + s.eng.Errorw("failed to marshal node-platform build info", "err", err) + return + } + + emitter := s.emitter + if emitter == nil { + emitter = beholder.GetEmitter() + } + + err = emitter.Emit(ctx, payloadBytes, + beholder.AttrKeyDomain, nodePlatformDomain, + beholder.AttrKeyEntity, nodePlatformEntity, + beholder.AttrKeyDataSchema, nodePlatformDataSchema, + ) + if err != nil { + s.eng.Errorw("failed to emit node-platform build info", "err", err) + } +} + +func (s *NodePlatformBuildInfoService) GetBeat() time.Duration { + return s.beat +} diff --git a/core/services/chainlink/node_platform_test.go b/core/services/chainlink/node_platform_test.go new file mode 100644 index 00000000000..c6419c054d9 --- /dev/null +++ b/core/services/chainlink/node_platform_test.go @@ -0,0 +1,80 @@ +package chainlink_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/csakey" + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/beholder/beholdertest" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" + commonv1 "github.com/smartcontractkit/chainlink-protos/node-platform/common/v1" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + keystoremocks "github.com/smartcontractkit/chainlink/v2/core/services/keystore/mocks" +) + +func TestNodePlatformBuildInfo_EmitsNodeBuildInfo(t *testing.T) { + obs := beholdertest.NewObserver(t) + + servicetest.Run(t, chainlink.NewNodePlatformBuildInfoService(chainlink.NodePlatformBuildInfoConfig{ + Beat: 10 * time.Millisecond, + Lggr: logger.TestLogger(t), + CSAPublicKey: "csa-public-key", + CommitSHA: "commit-sha", + DockerTag: "docker-tag", + VersionTag: "version-tag", + Version: "1.2.3", + })) + + require.Eventually(t, func() bool { + return obs.Len(t, beholder.AttrKeyEntity, "common.v1.NodeBuildInfo") > 0 + }, time.Second, 10*time.Millisecond) + + msgs := obs.Messages(t, beholder.AttrKeyEntity, "common.v1.NodeBuildInfo") + require.NotEmpty(t, msgs) + + msg := msgs[0] + require.Equal(t, "node-platform", msg.Attrs[beholder.AttrKeyDomain]) + require.Equal(t, "/node-platform/common/v1", msg.Attrs[beholder.AttrKeyDataSchema]) + + var payload commonv1.NodeBuildInfo + require.NoError(t, proto.Unmarshal(msg.Body, &payload)) + require.Equal(t, "csa-public-key", payload.CsaPublicKey) + require.Equal(t, "commit-sha", payload.CommitSha) + require.Equal(t, "docker-tag", payload.DockerTag) + require.Equal(t, "version-tag", payload.VersionTag) + require.Equal(t, "1.2.3", payload.Version) +} + +func TestNodePlatformBuildInfo_ResolvesCSAKeyOnStart(t *testing.T) { + obs := beholdertest.NewObserver(t) + csaStore := &keystoremocks.CSA{} + + csaStore.EXPECT().EnsureKey(mock.Anything).Return(nil).Once() + csaStore.EXPECT().GetAll().Return([]csakey.KeyV2{cltest.DefaultCSAKey}, nil).Once() + + servicetest.Run(t, chainlink.NewNodePlatformBuildInfoService(chainlink.NodePlatformBuildInfoConfig{ + Beat: 10 * time.Millisecond, + Lggr: logger.TestLogger(t), + CSAKeyStore: csaStore, + CommitSHA: "commit-sha", + DockerTag: "docker-tag", + VersionTag: "version-tag", + Version: "1.2.3", + })) + + require.Eventually(t, func() bool { + return obs.Len(t, beholder.AttrKeyEntity, "common.v1.NodeBuildInfo") > 0 + }, time.Second, 10*time.Millisecond) + + msg := obs.Messages(t, beholder.AttrKeyEntity, "common.v1.NodeBuildInfo")[0] + var payload commonv1.NodeBuildInfo + require.NoError(t, proto.Unmarshal(msg.Body, &payload)) + require.Equal(t, cltest.DefaultCSAKey.PublicKeyString(), payload.CsaPublicKey) +} diff --git a/core/services/gateway/handlers/vault/aggregator.go b/core/services/gateway/handlers/vault/aggregator.go index ef3593bfcbc..3e51e5cbefb 100644 --- a/core/services/gateway/handlers/vault/aggregator.go +++ b/core/services/gateway/handlers/vault/aggregator.go @@ -139,11 +139,6 @@ func (a *baseAggregator) validateUsingSignatures(don capabilities.DON, nodes []c return nil, errors.New("response result and error both are is nil: cannot validate signatures") } - if resp.Method == vaulttypes.MethodSecretsGet { - // SecretsGet responses are not signed. - return resp, errors.New("cannot validate signatures for Get requests") - } - r := &vaulttypes.SignedOCRResponse{} err := json.Unmarshal(*resp.Result, r) if err != nil { diff --git a/core/services/gateway/handlers/vault/aggregator_test.go b/core/services/gateway/handlers/vault/aggregator_test.go index 9d6680d0401..84b6cfbfba3 100644 --- a/core/services/gateway/handlers/vault/aggregator_test.go +++ b/core/services/gateway/handlers/vault/aggregator_test.go @@ -99,7 +99,7 @@ func newMessage(t *testing.T) *jsonrpc.Response[json.RawMessage] { return &jsonrpc.Response[json.RawMessage]{ Version: jsonrpc.JsonRpcVersion, ID: "1", - Method: vaulttypes.MethodSecretsGet, + Method: vaulttypes.MethodSecretsCreate, Result: (*json.RawMessage)(&rawResp), } } @@ -119,7 +119,7 @@ func TestAggregator_Valid_FallsBackToQuorum(t *testing.T) { currResp := jsonrpc.Response[json.RawMessage]{ Version: jsonrpc.JsonRpcVersion, ID: "1", - Method: vaulttypes.MethodSecretsGet, + Method: vaulttypes.MethodSecretsCreate, Result: (*json.RawMessage)(nil), Error: &jsonrpc.WireError{ Code: 123, @@ -180,7 +180,7 @@ func TestAggregator_InsufficientResponses(t *testing.T) { currResp := jsonrpc.Response[json.RawMessage]{ Version: jsonrpc.JsonRpcVersion, ID: "1", - Method: vaulttypes.MethodSecretsGet, + Method: vaulttypes.MethodSecretsCreate, Result: &rm, } responses := map[string]jsonrpc.Response[json.RawMessage]{ @@ -206,21 +206,21 @@ func TestAggregator_QuorumUnobtainable(t *testing.T) { resp1 := &jsonrpc.Response[json.RawMessage]{ Version: jsonrpc.JsonRpcVersion, ID: "1", - Method: vaulttypes.MethodSecretsGet, + Method: vaulttypes.MethodSecretsCreate, Result: &rm1, } rm2 := json.RawMessage([]byte(`{"foo": "bar"}`)) resp2 := &jsonrpc.Response[json.RawMessage]{ Version: jsonrpc.JsonRpcVersion, ID: "1", - Method: vaulttypes.MethodSecretsGet, + Method: vaulttypes.MethodSecretsCreate, Result: &rm2, } rm3 := json.RawMessage([]byte(`{"baz": "qux"}`)) resp3 := &jsonrpc.Response[json.RawMessage]{ Version: jsonrpc.JsonRpcVersion, ID: "1", - Method: vaulttypes.MethodSecretsGet, + Method: vaulttypes.MethodSecretsCreate, Result: &rm3, } responses := map[string]jsonrpc.Response[json.RawMessage]{ diff --git a/core/services/gateway/handlers/vault/handler.go b/core/services/gateway/handlers/vault/handler.go index f073999feb7..1168c27a2ef 100644 --- a/core/services/gateway/handlers/vault/handler.go +++ b/core/services/gateway/handlers/vault/handler.go @@ -326,7 +326,7 @@ func (h *handler) removeExpiredRequests(ctx context.Context) { } func (h *handler) Methods() []string { - return vaulttypes.GetSupportedMethods(h.lggr) + return vaulttypes.Methods } func (h *handler) HandleLegacyUserMessage(_ context.Context, _ *api.Message, _ gwhandlers.Callback) error { @@ -361,9 +361,6 @@ func (h *handler) HandleJSONRPCUserMessage(ctx context.Context, req jsonrpc.Requ h.lggr.Debugw("returning cached public key response") return h.handlePublicKeyGetSynchronously(ctx, req, publicKeyResponseBytes, callback) - case vaulttypes.MethodSecretsGet: - h.lggr.Errorw("Get requests not allowed", "requestID", req.ID) - return errors.New("get request not allowed") } isAuthorized, owner, err := h.requestAuthorizer.AuthorizeRequest(ctx, req) diff --git a/core/services/llo/bm/dummy_transmitter_test.go b/core/services/llo/bm/dummy_transmitter_test.go index e6c59e2f9e9..349c346d708 100644 --- a/core/services/llo/bm/dummy_transmitter_test.go +++ b/core/services/llo/bm/dummy_transmitter_test.go @@ -9,21 +9,20 @@ import ( "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" - - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" - "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) func Test_DummyTransmitter(t *testing.T) { - lggr, observedLogs := logger.TestLoggerObserved(t, zapcore.DebugLevel) + lggr, observedLogs := logger.TestObservedSugared(t, zapcore.DebugLevel) tr := NewTransmitter(lggr, "dummy") servicetest.Run(t, tr) err := tr.Transmit( - testutils.Context(t), + t.Context(), types.ConfigDigest{}, 42, ocr3types.ReportWithInfo[llotypes.ReportInfo]{}, @@ -31,5 +30,5 @@ func Test_DummyTransmitter(t *testing.T) { ) require.NoError(t, err) - testutils.RequireLogMessage(t, observedLogs, "Transmit") + tests.RequireLogMessage(t, observedLogs, "Transmit") } diff --git a/core/services/llo/channeldefinitions/channel_definition_cache_factory_test.go b/core/services/llo/channeldefinitions/channel_definition_cache_factory_test.go index 024f2b1fff6..7f853c84007 100644 --- a/core/services/llo/channeldefinitions/channel_definition_cache_factory_test.go +++ b/core/services/llo/channeldefinitions/channel_definition_cache_factory_test.go @@ -6,12 +6,13 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + lloconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/llo/config" ) func Test_ChannelDefinitionCacheFactory(t *testing.T) { - lggr := logger.TestLogger(t) + lggr := logger.Test(t) cdcFactory := NewChannelDefinitionCacheFactory(lggr, nil, nil, nil) t.Run("NewCache", func(t *testing.T) { diff --git a/core/services/llo/channeldefinitions/onchain_channel_definition_cache.go b/core/services/llo/channeldefinitions/onchain_channel_definition_cache.go index 551806283ce..931f7dd0a90 100644 --- a/core/services/llo/channeldefinitions/onchain_channel_definition_cache.go +++ b/core/services/llo/channeldefinitions/onchain_channel_definition_cache.go @@ -21,21 +21,21 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/jpillora/backoff" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "golang.org/x/crypto/sha3" + clhttp "github.com/smartcontractkit/chainlink-common/pkg/http" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" "github.com/smartcontractkit/chainlink-common/pkg/types/query" "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" - - clhttp "github.com/smartcontractkit/chainlink-common/pkg/http" "github.com/smartcontractkit/chainlink-evm/gethwrappers/llo-feeds/generated/channel_config_store" "github.com/smartcontractkit/chainlink-evm/pkg/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/services/llo/types" - "github.com/smartcontractkit/chainlink/v2/core/utils" ) const ( @@ -667,7 +667,7 @@ func (c *channelDefinitionCache) fetchLatestLoop() { func (c *channelDefinitionCache) fetchLoop(trigger types.Trigger) { defer c.wg.Done() var err error - b := utils.NewHTTPFetchBackoff() + b := newHTTPFetchBackoff() ctx, cancel := c.chStop.CtxWithTimeout(fetchRetryTimeout) defer cancel() @@ -970,3 +970,11 @@ func decodePersistedSourceDefinitions(definitionsJSON json.RawMessage) (map[uint return sources, nil } + +func newHTTPFetchBackoff() backoff.Backoff { + return backoff.Backoff{ + Min: 100 * time.Millisecond, + Max: 15 * time.Second, + Jitter: true, + } +} diff --git a/core/services/llo/channeldefinitions/onchain_channel_definition_cache_test.go b/core/services/llo/channeldefinitions/onchain_channel_definition_cache_test.go index bc738452428..2d635c5b28d 100644 --- a/core/services/llo/channeldefinitions/onchain_channel_definition_cache_test.go +++ b/core/services/llo/channeldefinitions/onchain_channel_definition_cache_test.go @@ -20,7 +20,8 @@ import ( llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" "github.com/smartcontractkit/chainlink-common/pkg/types/query" "github.com/smartcontractkit/chainlink-evm/pkg/logpoller" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink-evm/pkg/testutils" + "github.com/smartcontractkit/chainlink/v2/core/services/llo/types" ) diff --git a/core/services/llo/channeldefinitions/static_channel_definitions_cache.go b/core/services/llo/channeldefinitions/static_channel_definitions_cache.go index d9758f9ec55..8def75e8563 100644 --- a/core/services/llo/channeldefinitions/static_channel_definitions_cache.go +++ b/core/services/llo/channeldefinitions/static_channel_definitions_cache.go @@ -5,10 +5,9 @@ import ( "encoding/json" "fmt" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" ) // A CDC that loads a static JSON of channel definitions; useful for diff --git a/core/services/llo/cleanup_test.go b/core/services/llo/cleanup_test.go index 028c1180199..7b0d668d9c7 100644 --- a/core/services/llo/cleanup_test.go +++ b/core/services/llo/cleanup_test.go @@ -14,10 +14,9 @@ import ( "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/chainlink-common/pkg/logger" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" - "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/llo/mercurytransmitter" ) @@ -55,7 +54,7 @@ func makeSampleTransmission(seqNr uint64, sURL string) *mercurytransmitter.Trans } func Test_Cleanup(t *testing.T) { - ctx := testutils.Context(t) + ctx := t.Context() lp := &mockLogPoller{} ds := pgtest.NewSqlxDB(t) @@ -124,15 +123,15 @@ func Test_Cleanup(t *testing.T) { func Test_StaleTransmissionReaper(t *testing.T) { ds := pgtest.NewSqlxDB(t) - lggr := logger.TestLogger(t) + lggr := logger.Test(t) tr := &transmissionReaper{ds: ds, lggr: lggr, maxAge: 24 * time.Hour} - ctx := testutils.Context(t) + ctx := t.Context() const n = 13 transmissions := makeSampleTransmissions(n) torm := mercurytransmitter.NewORM(ds, 1) - err := torm.Insert(testutils.Context(t), transmissions) + err := torm.Insert(t.Context(), transmissions) require.NoError(t, err) pgtest.MustExec(t, ds, ` UPDATE llo_mercury_transmit_queue @@ -157,9 +156,9 @@ WHERE transmission_hash IN ( func Test_OrphanedTransmissionReaper(t *testing.T) { ds := pgtest.NewSqlxDB(t) - lggr := logger.TestLogger(t) + lggr := logger.Test(t) tr := &transmissionReaper{ds: ds, lggr: lggr, maxAge: 24 * time.Hour} - ctx := testutils.Context(t) + ctx := t.Context() const n = 13 @@ -171,7 +170,7 @@ func Test_OrphanedTransmissionReaper(t *testing.T) { // add transmissions from a DON not present in ocr2 specs transmissions := makeSampleTransmissions(n) torm := mercurytransmitter.NewORM(ds, 1) - err := torm.Insert(testutils.Context(t), transmissions) + err := torm.Insert(t.Context(), transmissions) require.NoError(t, err) d, err := tr.reap(ctx, n, "orphaned") @@ -179,7 +178,7 @@ func Test_OrphanedTransmissionReaper(t *testing.T) { assert.Equal(t, int64(n), d) torm2 := mercurytransmitter.NewORM(ds, 2) - err = torm2.Insert(testutils.Context(t), transmissions) + err = torm2.Insert(t.Context(), transmissions) require.NoError(t, err) d, err = tr.reap(ctx, n, "orphaned") diff --git a/core/services/llo/cre/transmitter_test.go b/core/services/llo/cre/transmitter_test.go index 6d0e9295169..b2e928f0750 100644 --- a/core/services/llo/cre/transmitter_test.go +++ b/core/services/llo/cre/transmitter_test.go @@ -10,12 +10,12 @@ import ( "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/logger" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" "github.com/smartcontractkit/chainlink-data-streams/llo" datastreamsllo "github.com/smartcontractkit/chainlink-data-streams/llo" "github.com/smartcontractkit/chainlink-protos/cre/go/values" streamstypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/streams" - "github.com/smartcontractkit/chainlink/v2/core/logger" ) const ( @@ -32,7 +32,7 @@ func Test_Transmitter(t *testing.T) { } cfg := TransmitterConfig{ - Logger: logger.TestLogger(t), + Logger: logger.Test(t), CapabilitiesRegistry: nil, DonID: donID, } @@ -79,7 +79,7 @@ func buildRegistrationRequest(t *testing.T, triggerID string, streamIDs []stream } func encodeReport(t *testing.T, timestamp uint64) ocr3types.ReportWithInfo[llotypes.ReportInfo] { - codec := NewReportCodecCapabilityTrigger(logger.TestLogger(t), donID) + codec := NewReportCodecCapabilityTrigger(logger.Test(t), donID) rep := llo.Report{ ConfigDigest: types.ConfigDigest{1, 2, 3}, SeqNr: 32, diff --git a/core/services/llo/keyring.go b/core/services/llo/keyring.go index 5d9fb673806..e7a3f11314f 100644 --- a/core/services/llo/keyring.go +++ b/core/services/llo/keyring.go @@ -13,7 +13,7 @@ import ( llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" "github.com/smartcontractkit/chainlink-data-streams/llo/reportcodecs/evm" - "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/v2/core/utils/crypto" ) @@ -43,7 +43,7 @@ type onchainKeyring struct { func NewOnchainKeyring(lggr logger.Logger, keys map[llotypes.ReportFormat]Key, donID uint32) LLOOnchainKeyring { return &onchainKeyring{ - lggr.Named("OnchainKeyring"), keys, donID, + logger.Sugared(lggr).Named("OnchainKeyring"), keys, donID, } } diff --git a/core/services/llo/keyring_test.go b/core/services/llo/keyring_test.go index 92ff8f361a5..faf5a7e6f90 100644 --- a/core/services/llo/keyring_test.go +++ b/core/services/llo/keyring_test.go @@ -1,6 +1,7 @@ package llo import ( + crand "crypto/rand" "fmt" "math" "math/rand/v2" @@ -15,8 +16,7 @@ import ( llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" - "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink-common/pkg/logger" ) var _ Key = &mockKey{} @@ -63,7 +63,7 @@ func (m *mockKey) reset(format llotypes.ReportFormat) { } func Test_Keyring(t *testing.T) { - lggr := logger.TestLogger(t) + lggr := logger.Test(t) ks := map[llotypes.ReportFormat]Key{ llotypes.ReportFormatEVMPremiumLegacy: &mockKey{format: llotypes.ReportFormatEVMPremiumLegacy, maxSignatureLen: 1, sig: []byte("sig-1")}, @@ -87,7 +87,7 @@ func Test_Keyring(t *testing.T) { }, } - cd, err := ocrtypes.BytesToConfigDigest(testutils.MustRandBytes(32)) + cd, err := ocrtypes.BytesToConfigDigest(mustRandBytes(32)) require.NoError(t, err) seqNr := rand.Uint64N(math.MaxUint32 << 8) t.Run("Sign+Verify", func(t *testing.T) { @@ -119,3 +119,12 @@ func Test_Keyring(t *testing.T) { assert.Equal(t, types.OnchainPublicKey(b), kr.PublicKey()) }) } + +func mustRandBytes(n int) (b []byte) { + b = make([]byte, n) + _, err := crand.Read(b) + if err != nil { + panic(err) + } + return +} diff --git a/core/services/llo/mercurytransmitter/orm_test.go b/core/services/llo/mercurytransmitter/orm_test.go index f36b32d3af6..69950427b37 100644 --- a/core/services/llo/mercurytransmitter/orm_test.go +++ b/core/services/llo/mercurytransmitter/orm_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" ) @@ -18,7 +17,7 @@ var ( ) func TestORM(t *testing.T) { - ctx := testutils.Context(t) + ctx := t.Context() db := pgtest.NewSqlxDB(t) t.Run("Insert, Get, Delete, Prune", func(t *testing.T) { diff --git a/core/services/llo/mercurytransmitter/persistence_manager_test.go b/core/services/llo/mercurytransmitter/persistence_manager_test.go index 8fecd30d3fa..dd3d540f71b 100644 --- a/core/services/llo/mercurytransmitter/persistence_manager_test.go +++ b/core/services/llo/mercurytransmitter/persistence_manager_test.go @@ -13,15 +13,16 @@ import ( ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" - "github.com/smartcontractkit/chainlink/v2/core/logger" ) func bootstrapPersistenceManager(t *testing.T, donID uint32, db *sqlx.DB, maxTransmitQueueSize int) (*persistenceManager, *observer.ObservedLogs) { t.Helper() - lggr, observedLogs := logger.TestLoggerObserved(t, zapcore.DebugLevel) + lggr, observedLogs := logger.TestObservedSugared(t, zapcore.DebugLevel) orm := NewORM(db, donID) return NewPersistenceManager(lggr, orm, "wss://example.com/mercury", maxTransmitQueueSize, 5*time.Millisecond, 5*time.Millisecond, 30*24*time.Hour), observedLogs } @@ -30,7 +31,7 @@ func TestPersistenceManager(t *testing.T) { donID1 := uint32(1234) donID2 := uint32(2345) - ctx := testutils.Context(t) + ctx := t.Context() db := pgtest.NewSqlxDB(t) t.Run("loads transmissions", func(t *testing.T) { @@ -82,7 +83,7 @@ func TestPersistenceManager(t *testing.T) { } func TestPersistenceManagerAsyncDelete(t *testing.T) { - ctx := testutils.Context(t) + ctx := t.Context() donID := uint32(1234) db := pgtest.NewSqlxDB(t) pm, observedLogs := bootstrapPersistenceManager(t, donID, db, 1000) @@ -97,7 +98,7 @@ func TestPersistenceManagerAsyncDelete(t *testing.T) { // Wait for next poll. observedLogs.TakeAll() - testutils.WaitForLogMessage(t, observedLogs, "Flushed delete queue") + tests.AssertLogEventually(t, observedLogs, "Flushed delete queue") result, err := pm.Load(ctx) require.NoError(t, err) @@ -110,7 +111,7 @@ func TestPersistenceManagerPrune(t *testing.T) { donID2 := uint32(654321) db := pgtest.NewSqlxDB(t) - ctx := testutils.Context(t) + ctx := t.Context() transmissions := make([]*Transmission, 45) for i := range uint64(45) { @@ -131,7 +132,7 @@ func TestPersistenceManagerPrune(t *testing.T) { // Wait for next poll. observedLogs.TakeAll() - testutils.WaitForLogMessage(t, observedLogs, "Pruned transmit requests table") + tests.AssertLogEventually(t, observedLogs, "Pruned transmit requests table") result, err := pm.Load(ctx) require.NoError(t, err) @@ -159,7 +160,7 @@ func Test_PersistenceManager_deleteTransmissions(t *testing.T) { donID1 := uint32(123456) db := pgtest.NewSqlxDB(t) - ctx := testutils.Context(t) + ctx := t.Context() transmissions := make([]*Transmission, 45) for i := range uint64(45) { diff --git a/core/services/llo/mercurytransmitter/queue_test.go b/core/services/llo/mercurytransmitter/queue_test.go index 1df1bed4421..31eeb20451a 100644 --- a/core/services/llo/mercurytransmitter/queue_test.go +++ b/core/services/llo/mercurytransmitter/queue_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" - "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) var _ asyncDeleter = &mockAsyncDeleter{} @@ -31,7 +31,7 @@ func Test_Queue(t *testing.T) { t.Parallel() const maxSize = 7 - lggr, observedLogs := logger.TestLoggerObserved(t, zapcore.ErrorLevel) + lggr, observedLogs := logger.TestObserved(t, zapcore.ErrorLevel) t.Run("cannot init with more transmissions than capacity", func(t *testing.T) { transmissions := makeSampleTransmissions(maxSize+1, sURL) @@ -74,7 +74,7 @@ func Test_Queue(t *testing.T) { } // expecting testTransmissions[0] to get evicted and not present in the queue anymore - testutils.WaitForLogMessage(t, observedLogs, "Transmit queue is full; dropping oldest transmission (reached max length of 7)") + tests.AssertLogEventually(t, observedLogs, "Transmit queue is full; dropping oldest transmission (reached max length of 7)") var transmissions []*Transmission for range 7 { tr := tq.BlockingPop() diff --git a/core/services/llo/mercurytransmitter/server.go b/core/services/llo/mercurytransmitter/server.go index 40fb1bbd55e..0bd66fd373c 100644 --- a/core/services/llo/mercurytransmitter/server.go +++ b/core/services/llo/mercurytransmitter/server.go @@ -21,12 +21,10 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" + "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-data-streams/llo" "github.com/smartcontractkit/chainlink-data-streams/llo/reportcodecs/evm" "github.com/smartcontractkit/chainlink-data-streams/rpc" - - corelogger "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/utils" ) var ( @@ -112,7 +110,7 @@ func newServer(lggr logger.Logger, verboseLogging bool, cfg QueueConfig, client if verboseLogging { codecLggr = lggr } else { - codecLggr = corelogger.NullLogger + codecLggr = logger.Nop() } s := &server{ diff --git a/core/services/llo/mercurytransmitter/transmitter_test.go b/core/services/llo/mercurytransmitter/transmitter_test.go index eb773a7c9d5..3c7984fb60e 100644 --- a/core/services/llo/mercurytransmitter/transmitter_test.go +++ b/core/services/llo/mercurytransmitter/transmitter_test.go @@ -15,12 +15,12 @@ import ( "github.com/smartcontractkit/libocr/commontypes" "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink-data-streams/rpc" "github.com/smartcontractkit/chainlink/v2/core/config" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" - "github.com/smartcontractkit/chainlink/v2/core/logger" ) type mockCfg struct{} @@ -60,7 +60,7 @@ func (m *MockGRPCClient) Transmit(ctx context.Context, in *rpc.TransmitRequest) func (m *MockGRPCClient) ServerURL() string { return "mock server url" } func Test_Transmitter_Transmit(t *testing.T) { - lggr := logger.TestLogger(t) + lggr := logger.Test(t) db := pgtest.NewSqlxDB(t) donID := uint32(123456) orm := NewORM(db, donID) @@ -83,7 +83,7 @@ func Test_Transmitter_Transmit(t *testing.T) { Signature: []byte{22}, Signer: commontypes.OracleID(43), }} - err := mt.Transmit(testutils.Context(t), digest, seqNr, report, sigs) + err := mt.Transmit(t.Context(), digest, seqNr, report, sigs) require.Error(t, err) assert.Contains(t, err.Error(), "transmitter is not started") }) @@ -121,7 +121,7 @@ func Test_Transmitter_Transmit(t *testing.T) { Signature: []byte{22}, Signer: commontypes.OracleID(43), }} - err = mt.Transmit(testutils.Context(t), digest, seqNr, report, sigs) + err = mt.Transmit(t.Context(), digest, seqNr, report, sigs) require.NoError(t, err) // wait for the commit loop to run @@ -185,7 +185,7 @@ func (m *mockQ) IsEmpty() bool { return false } func Test_Transmitter_runQueueLoop(t *testing.T) { donIDStr := "555" - lggr := logger.TestLogger(t) + lggr := logger.Test(t) c := &MockGRPCClient{} db := pgtest.NewSqlxDB(t) donID := uint32(123456) @@ -214,7 +214,7 @@ func Test_Transmitter_runQueueLoop(t *testing.T) { case tr := <-transmit: assert.Equal(t, []byte{0x0, 0x9, 0x57, 0xdd, 0x2f, 0x63, 0x56, 0x69, 0x34, 0xfd, 0xc2, 0xe1, 0xcd, 0xc1, 0xe, 0x3e, 0x25, 0xb9, 0x26, 0x5a, 0x16, 0x23, 0x91, 0xa6, 0x53, 0x16, 0x66, 0x59, 0x51, 0x0, 0x28, 0x7c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xe2, 0x40, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x80, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x20, 0x0, 0x3, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x66, 0xde, 0xf5, 0xba, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x66, 0xde, 0xf5, 0xba, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1e, 0x8e, 0x95, 0xcf, 0xb5, 0xd8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0xd0, 0x1c, 0x67, 0xa9, 0xcf, 0xb3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x66, 0xdf, 0x3, 0xca, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1b, 0x1c, 0x93, 0x6d, 0xa4, 0xf2, 0x17, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1b, 0x14, 0x8d, 0x9a, 0xc1, 0xd9, 0x6f, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1b, 0x40, 0x5c, 0xcf, 0xa1, 0xbc, 0x63, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x9d, 0xab, 0x8f, 0xa7, 0xca, 0x7, 0x62, 0x57, 0xf7, 0x11, 0x2c, 0xb7, 0xf3, 0x49, 0x37, 0x12, 0xbd, 0xe, 0x14, 0x27, 0xfc, 0x32, 0x5c, 0xec, 0xa6, 0xb9, 0x7f, 0xf9, 0xd7, 0x7b, 0xa6, 0x36, 0x9a, 0x47, 0x4a, 0x3, 0x1a, 0x95, 0xcf, 0x46, 0x10, 0xaf, 0xcc, 0x90, 0x49, 0xb2, 0xce, 0xbf, 0x63, 0xaa, 0xc7, 0x25, 0x4d, 0x2a, 0x8, 0x36, 0xda, 0xd5, 0x9f, 0x9d, 0x63, 0x69, 0x22, 0xb3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x30, 0x9d, 0x84, 0x29, 0xbf, 0xd4, 0xeb, 0xc5, 0xc9, 0x29, 0xef, 0xdd, 0xd3, 0x2f, 0xa6, 0x25, 0x63, 0xda, 0xd9, 0x2c, 0xa1, 0x4a, 0xba, 0x75, 0xb2, 0x85, 0x25, 0x8f, 0x2b, 0x84, 0xcd, 0x99, 0x36, 0xd9, 0x6e, 0xf, 0xae, 0x7b, 0xd1, 0x61, 0x59, 0xf, 0x36, 0x4a, 0x22, 0xec, 0xde, 0x45, 0x32, 0xe0, 0x5b, 0x5c, 0xe3, 0x14, 0x29, 0x4, 0x60, 0x7b, 0xce, 0xa3, 0x89, 0x6b, 0xbb, 0xe0}, tr.Payload) assert.Equal(t, int(transmission.Report.Info.ReportFormat), int(tr.ReportFormat)) - case <-time.After(testutils.WaitTimeout(t)): + case <-time.After(tests.WaitTimeout(t)): t.Fatal("expected a transmit request to be sent") } @@ -242,7 +242,7 @@ func Test_Transmitter_runQueueLoop(t *testing.T) { case tr := <-transmit: assert.Equal(t, []byte{0x0, 0x9, 0x57, 0xdd, 0x2f, 0x63, 0x56, 0x69, 0x34, 0xfd, 0xc2, 0xe1, 0xcd, 0xc1, 0xe, 0x3e, 0x25, 0xb9, 0x26, 0x5a, 0x16, 0x23, 0x91, 0xa6, 0x53, 0x16, 0x66, 0x59, 0x51, 0x0, 0x28, 0x7c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xe2, 0x40, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x80, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x20, 0x0, 0x3, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x66, 0xde, 0xf5, 0xba, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x66, 0xde, 0xf5, 0xba, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1e, 0x8e, 0x95, 0xcf, 0xb5, 0xd8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0xd0, 0x1c, 0x67, 0xa9, 0xcf, 0xb3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x66, 0xdf, 0x3, 0xca, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1b, 0x1c, 0x93, 0x6d, 0xa4, 0xf2, 0x17, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1b, 0x14, 0x8d, 0x9a, 0xc1, 0xd9, 0x6f, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1b, 0x40, 0x5c, 0xcf, 0xa1, 0xbc, 0x63, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x9d, 0xab, 0x8f, 0xa7, 0xca, 0x7, 0x62, 0x57, 0xf7, 0x11, 0x2c, 0xb7, 0xf3, 0x49, 0x37, 0x12, 0xbd, 0xe, 0x14, 0x27, 0xfc, 0x32, 0x5c, 0xec, 0xa6, 0xb9, 0x7f, 0xf9, 0xd7, 0x7b, 0xa6, 0x36, 0x9a, 0x47, 0x4a, 0x3, 0x1a, 0x95, 0xcf, 0x46, 0x10, 0xaf, 0xcc, 0x90, 0x49, 0xb2, 0xce, 0xbf, 0x63, 0xaa, 0xc7, 0x25, 0x4d, 0x2a, 0x8, 0x36, 0xda, 0xd5, 0x9f, 0x9d, 0x63, 0x69, 0x22, 0xb3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x30, 0x9d, 0x84, 0x29, 0xbf, 0xd4, 0xeb, 0xc5, 0xc9, 0x29, 0xef, 0xdd, 0xd3, 0x2f, 0xa6, 0x25, 0x63, 0xda, 0xd9, 0x2c, 0xa1, 0x4a, 0xba, 0x75, 0xb2, 0x85, 0x25, 0x8f, 0x2b, 0x84, 0xcd, 0x99, 0x36, 0xd9, 0x6e, 0xf, 0xae, 0x7b, 0xd1, 0x61, 0x59, 0xf, 0x36, 0x4a, 0x22, 0xec, 0xde, 0x45, 0x32, 0xe0, 0x5b, 0x5c, 0xe3, 0x14, 0x29, 0x4, 0x60, 0x7b, 0xce, 0xa3, 0x89, 0x6b, 0xbb, 0xe0}, tr.Payload) assert.Equal(t, int(transmission.Report.Info.ReportFormat), int(tr.ReportFormat)) - case <-time.After(testutils.WaitTimeout(t)): + case <-time.After(tests.WaitTimeout(t)): t.Fatal("expected a transmit request to be sent") } @@ -269,7 +269,7 @@ func Test_Transmitter_runQueueLoop(t *testing.T) { case tr := <-transmit: assert.Equal(t, []byte{0x0, 0x9, 0x57, 0xdd, 0x2f, 0x63, 0x56, 0x69, 0x34, 0xfd, 0xc2, 0xe1, 0xcd, 0xc1, 0xe, 0x3e, 0x25, 0xb9, 0x26, 0x5a, 0x16, 0x23, 0x91, 0xa6, 0x53, 0x16, 0x66, 0x59, 0x51, 0x0, 0x28, 0x7c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xe2, 0x40, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x80, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x20, 0x0, 0x3, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x66, 0xde, 0xf5, 0xba, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x66, 0xde, 0xf5, 0xba, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1e, 0x8e, 0x95, 0xcf, 0xb5, 0xd8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a, 0xd0, 0x1c, 0x67, 0xa9, 0xcf, 0xb3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x66, 0xdf, 0x3, 0xca, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1b, 0x1c, 0x93, 0x6d, 0xa4, 0xf2, 0x17, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1b, 0x14, 0x8d, 0x9a, 0xc1, 0xd9, 0x6f, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1b, 0x40, 0x5c, 0xcf, 0xa1, 0xbc, 0x63, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x9d, 0xab, 0x8f, 0xa7, 0xca, 0x7, 0x62, 0x57, 0xf7, 0x11, 0x2c, 0xb7, 0xf3, 0x49, 0x37, 0x12, 0xbd, 0xe, 0x14, 0x27, 0xfc, 0x32, 0x5c, 0xec, 0xa6, 0xb9, 0x7f, 0xf9, 0xd7, 0x7b, 0xa6, 0x36, 0x9a, 0x47, 0x4a, 0x3, 0x1a, 0x95, 0xcf, 0x46, 0x10, 0xaf, 0xcc, 0x90, 0x49, 0xb2, 0xce, 0xbf, 0x63, 0xaa, 0xc7, 0x25, 0x4d, 0x2a, 0x8, 0x36, 0xda, 0xd5, 0x9f, 0x9d, 0x63, 0x69, 0x22, 0xb3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x30, 0x9d, 0x84, 0x29, 0xbf, 0xd4, 0xeb, 0xc5, 0xc9, 0x29, 0xef, 0xdd, 0xd3, 0x2f, 0xa6, 0x25, 0x63, 0xda, 0xd9, 0x2c, 0xa1, 0x4a, 0xba, 0x75, 0xb2, 0x85, 0x25, 0x8f, 0x2b, 0x84, 0xcd, 0x99, 0x36, 0xd9, 0x6e, 0xf, 0xae, 0x7b, 0xd1, 0x61, 0x59, 0xf, 0x36, 0x4a, 0x22, 0xec, 0xde, 0x45, 0x32, 0xe0, 0x5b, 0x5c, 0xe3, 0x14, 0x29, 0x4, 0x60, 0x7b, 0xce, 0xa3, 0x89, 0x6b, 0xbb, 0xe0}, tr.Payload) assert.Equal(t, int(transmission.Report.Info.ReportFormat), int(tr.ReportFormat)) - case <-time.After(testutils.WaitTimeout(t)): + case <-time.After(tests.WaitTimeout(t)): t.Fatal("expected a transmit request to be sent") } @@ -304,7 +304,7 @@ func Test_Transmitter_runQueueLoop(t *testing.T) { break Loop } cnt++ - case <-time.After(testutils.WaitTimeout(t)): + case <-time.After(tests.WaitTimeout(t)): t.Fatal("expected 3 transmit requests to be sent") } } diff --git a/core/services/llo/orm_test.go b/core/services/llo/orm_test.go index ce5964bb05c..ba929ae6dd1 100644 --- a/core/services/llo/orm_test.go +++ b/core/services/llo/orm_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/require" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" + "github.com/smartcontractkit/chainlink-evm/pkg/testutils" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/v2/core/services/llo/channeldefinitions" "github.com/smartcontractkit/chainlink/v2/core/services/llo/types" @@ -23,7 +23,7 @@ func Test_ORM(t *testing.T) { db := pgtest.NewSqlxDB(t) orm := NewChainScopedORM(db, ETHMainnetChainSelector) - ctx := testutils.Context(t) + ctx := t.Context() addr1 := testutils.NewAddress() addr2 := testutils.NewAddress() diff --git a/core/services/llo/report_codecs_test.go b/core/services/llo/report_codecs_test.go index ad7d732f7cc..3e005328a02 100644 --- a/core/services/llo/report_codecs_test.go +++ b/core/services/llo/report_codecs_test.go @@ -5,12 +5,12 @@ import ( "github.com/stretchr/testify/assert" + "github.com/smartcontractkit/chainlink-common/pkg/logger" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" - "github.com/smartcontractkit/chainlink/v2/core/logger" ) func Test_NewReportCodecs(t *testing.T) { - c := NewReportCodecs(logger.TestLogger(t), 1) + c := NewReportCodecs(logger.Test(t), 1) assert.Contains(t, c, llotypes.ReportFormatJSON, "expected JSON to be supported") assert.Contains(t, c, llotypes.ReportFormatEVMPremiumLegacy, "expected EVMPremiumLegacy to be supported") diff --git a/core/services/llo/retirement/retirement_report_cache_test.go b/core/services/llo/retirement/retirement_report_cache_test.go index 9d2a158e285..2ec0099938b 100644 --- a/core/services/llo/retirement/retirement_report_cache_test.go +++ b/core/services/llo/retirement/retirement_report_cache_test.go @@ -10,7 +10,7 @@ import ( ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink-common/pkg/logger" ) type mockORM struct { @@ -43,7 +43,7 @@ func Test_RetirementReportCache(t *testing.T) { t.Parallel() ctx := t.Context() - lggr := logger.TestLogger(t) + lggr := logger.Test(t) orm := &mockORM{ make(map[ocrtypes.ConfigDigest][]byte), make(map[ocrtypes.ConfigDigest]Config), diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index 831e257d6cc..ca1c7f226ba 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -720,13 +720,14 @@ func (d *Delegate) newServicesVaultPlugin( expiryDuration := cfg.RequestExpiryDuration.Duration() requestStoreHandler := requests.NewHandler(lggr, requestStore, clock, expiryDuration) lpk := vaultcap.NewLazyPublicKey() - vaultCapability, err := vaultcap.NewCapability(lggr, clock, expiryDuration, requestStoreHandler, vaultcap.NewRequestAuthorizer(lggr, syncer), capabilitiesRegistry, lpk, limitsFactory) + vaultCapability, err := vaultcap.NewCapability(lggr, clock, expiryDuration, requestStoreHandler, capabilitiesRegistry, lpk, limitsFactory) if err != nil { return nil, fmt.Errorf("failed to instantiate vault plugin: failed to create vault capability: %w", err) } srvs = append(srvs, vaultCapability) - handler, err := vaultcap.NewGatewayHandler(capabilitiesRegistry, vaultCapability, gwconnector, d.lggr) + requestAuthorizer := vaultcap.NewRequestAuthorizer(lggr, syncer) + handler, err := vaultcap.NewGatewayHandler(vaultCapability, gwconnector, requestAuthorizer, d.lggr) if err != nil { return nil, fmt.Errorf("failed to instantiate vault plugin: failed to create vault handler: %w", err) } diff --git a/core/services/ocr2/plugins/vault/plugin.go b/core/services/ocr2/plugins/vault/plugin.go index f5d357af76a..92c8e83b8af 100644 --- a/core/services/ocr2/plugins/vault/plugin.go +++ b/core/services/ocr2/plugins/vault/plugin.go @@ -12,6 +12,7 @@ import ( "regexp" "slices" "sort" + "time" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/nacl/box" @@ -136,6 +137,31 @@ func (r *ReportingPluginFactory) getKeyMaterial(ctx context.Context, instanceID return publicKey, privateKeyShare, nil } +const dkgPollInterval = 2 * time.Second + +// pollForKeyMaterial polls the DKG result package database until the key +// material for the given instance ID is available or the context is cancelled. +// This avoids returning an immediate error when the DKG protocol hasn't +// completed yet, which would trigger libocr's exponential backoff (up to 2 +// minutes between retries). By polling here within the MaxDurationInitialization +// window, the vault oracle can start as soon as the DKG result is written. +func (r *ReportingPluginFactory) pollForKeyMaterial(ctx context.Context, instanceID string) (publicKey *tdh2easy.PublicKey, privateKeyShare *tdh2easy.PrivateShare, err error) { + for { + publicKey, privateKeyShare, err = r.getKeyMaterial(ctx, instanceID) + if err == nil { + return publicKey, privateKeyShare, nil + } + + r.lggr.Debugw("DKG result package not yet available, will retry", "instanceID", instanceID, "error", err) + + select { + case <-ctx.Done(): + return nil, nil, fmt.Errorf("context cancelled while waiting for DKG key material (instanceID=%s): %w", instanceID, err) + case <-time.After(dkgPollInterval): + } + } +} + func initializePluginLimits(ctx context.Context, limitsFactory limits.Factory) (ocr3_1types.ReportingPluginLimits, error) { maxQueryBytes, err := cresettings.Default.VaultMaxQuerySizeLimit.GetOrDefault(ctx, limitsFactory.Settings) if err != nil { @@ -277,7 +303,7 @@ func (r *ReportingPluginFactory) NewReportingPlugin(ctx context.Context, config } r.lggr.Debugw("fetching key material for instance id", "instanceID", *configProto.DKGInstanceID) - publicKey, privateKeyShare, err := r.getKeyMaterial(ctx, *configProto.DKGInstanceID) + publicKey, privateKeyShare, err := r.pollForKeyMaterial(ctx, *configProto.DKGInstanceID) if err != nil { return nil, ocr3_1types.ReportingPluginInfo1{}, fmt.Errorf("could not get key material from DB: %w", err) } @@ -481,7 +507,7 @@ func (r *ReportingPlugin) Observation(ctx context.Context, seqNr uint64, aq type return nil, fmt.Errorf("could not fetch max batch size limit: %w", ierr2) } - if len(observedLocalQueue) > 2*l { + if len(observedLocalQueue) >= 2*l { r.lggr.Warnw("Observed local queue exceeds batch size limit, truncating", "queueSize", len(observedLocalQueue), "batchSizeLimit", 2*l) diff --git a/core/services/ocr2/plugins/vault/plugin_test.go b/core/services/ocr2/plugins/vault/plugin_test.go index 60e35cdfde7..b755fef86db 100644 --- a/core/services/ocr2/plugins/vault/plugin_test.go +++ b/core/services/ocr2/plugins/vault/plugin_test.go @@ -5394,6 +5394,87 @@ func TestPlugin_ValidateObservation_RejectsIfMoreThan2xBatchSize(t *testing.T) { require.ErrorContains(t, err, "invalid observation: too many pending queue items provided, have 4, want max 2") } +// TestPlugin_ValidateObservation_AcceptsFullPendingQueueObservation verifies that an observation +// with exactly 2*batchSize pending queue items (the maximum Observation can produce) is accepted. +func TestPlugin_ValidateObservation_AcceptsFullPendingQueueObservation(t *testing.T) { + lggr := logger.TestLogger(t) + store := requests.NewStore[*vaulttypes.Request]() + _, pk, shares, err := tdh2easy.GenerateKeys(1, 3) + require.NoError(t, err) + + batchSize := 1 // MaxBatchSize=1, so 2*batchSize=2 is the intended max pending queue items + r := &ReportingPlugin{ + lggr: lggr, + store: store, + onchainCfg: ocr3types.ReportingPluginConfig{ + N: 4, + F: 1, + }, + cfg: makeReportingPluginConfig( + t, + batchSize, + pk, + shares[0], + 1, + 1024, + 30, + 30, + 30, + 10, + ), + unmarshalBlob: mockUnmarshalBlob, + } + + seqNr := uint64(1) + rdr := &kv{ + m: make(map[string]response), + } + + req1 := &vaultcommon.ListSecretIdentifiersRequest{ + Owner: "owner", + Namespace: "main", + RequestId: "request-id", + } + areq1, err := anypb.New(req1) + require.NoError(t, err) + + // Build an observation with exactly 2*batchSize = 2 pending queue items. + // This is the maximum that Observation() can produce. + numItems := 2 * batchSize + pendingQueueItems := make([][]byte, numItems) + blobs := make([][]byte, numItems) + for i := range numItems { + pendingQueueItems[i] = []byte{} + blobs[i] = protoMarshal(t, &vaultcommon.StoredPendingQueueItem{ + Id: fmt.Sprintf("request-id-%d", i), + Item: areq1, + }) + } + + o1 := &vaultcommon.Observations{ + PendingQueueItems: pendingQueueItems, + } + + o1b, err := proto.Marshal(o1) + require.NoError(t, err) + + bf := &blobber{ + blobs: blobs, + } + + err = r.ValidateObservation( + t.Context(), + seqNr, + types.AttributedQuery{}, + types.AttributedObservation{ + Observer: 0, Observation: o1b, + }, + rdr, + bf, + ) + require.NoError(t, err) +} + func TestPlugin_ValidateObservation_GetSecretsRequest(t *testing.T) { lggr := logger.TestLogger(t) store := requests.NewStore[*vaulttypes.Request]() diff --git a/core/utils/utils.go b/core/utils/utils.go index 7326800a88d..a6abee89b44 100644 --- a/core/utils/utils.go +++ b/core/utils/utils.go @@ -478,14 +478,6 @@ func NewRedialBackoff() backoff.Backoff { } } -func NewHTTPFetchBackoff() backoff.Backoff { - return backoff.Backoff{ - Min: 100 * time.Millisecond, - Max: 15 * time.Second, - Jitter: true, - } -} - // ConcatBytes appends a bunch of byte arrays into a single byte array func ConcatBytes(bufs ...[]byte) []byte { return bytes.Join(bufs, []byte{}) diff --git a/core/web/testdata/body/health.html b/core/web/testdata/body/health.html index 7cccfcd4d6c..cf189621cf5 100644 --- a/core/web/testdata/body/health.html +++ b/core/web/testdata/body/health.html @@ -111,6 +111,9 @@ +
+ NodePlatformBuildInfo +
PipelineORM
@@ -147,4 +150,3 @@
WorkflowStore
- diff --git a/core/web/testdata/body/health.json b/core/web/testdata/body/health.json index 91704864a72..31054ef17e7 100644 --- a/core/web/testdata/body/health.json +++ b/core/web/testdata/body/health.json @@ -207,6 +207,15 @@ "output": "" } }, + { + "type": "checks", + "id": "NodePlatformBuildInfo", + "attributes": { + "name": "NodePlatformBuildInfo", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "PipelineORM", diff --git a/core/web/testdata/body/health.txt b/core/web/testdata/body/health.txt index 8818fc279d9..59a77f4a057 100644 --- a/core/web/testdata/body/health.txt +++ b/core/web/testdata/body/health.txt @@ -22,6 +22,7 @@ ok LLOTransmissionReaper ok Mailbox.Monitor ok Mercury.WSRPCPool ok Mercury.WSRPCPool.CacheSet +ok NodePlatformBuildInfo ok PipelineORM ok PipelineRunner ok PipelineRunner.BridgeCache diff --git a/deployment/cre/jobs/operations/propose_gateway_job.go b/deployment/cre/jobs/operations/propose_gateway_job.go index 4d0f83ab064..239b6dc0f15 100644 --- a/deployment/cre/jobs/operations/propose_gateway_job.go +++ b/deployment/cre/jobs/operations/propose_gateway_job.go @@ -26,8 +26,8 @@ type ProposeGatewayJobInput struct { ServiceCentricFormatEnabled bool `yaml:"serviceCentricFormatEnabled"` DONs []DON `yaml:"dons"` Services []GatewayService `yaml:"services"` - GatewayRequestTimeoutSec int `yaml:"gatewayRequestTimeoutSec"` - AllowedPorts []int `yaml:"allowedPorts"` + GatewayRequestTimeoutSec pkg.Int `yaml:"gatewayRequestTimeoutSec"` + AllowedPorts []pkg.Int `yaml:"allowedPorts"` AllowedSchemes []string `yaml:"allowedSchemes"` AllowedIPsCIDR []string `yaml:"allowedIPsCIDR"` AuthGatewayID string `yaml:"authGatewayID"` @@ -37,7 +37,7 @@ type ProposeGatewayJobInput struct { type DON struct { Name string `yaml:"name"` - F int `yaml:"f"` + F pkg.Int `yaml:"f"` Handlers []string `yaml:"handlers"` } @@ -66,7 +66,7 @@ var ProposeGatewayJob = operations.NewOperation[ProposeGatewayJobInput, ProposeG // When ServiceCentricFormatEnabled is true, it derives the set of unique DON names from // input.Services; otherwise it uses the don-centric input.DONs list. func proposeGatewayJob(b operations.Bundle, deps ProposeGatewayJobDeps, input ProposeGatewayJobInput) (ProposeGatewayJobOutput, error) { - requestTimeoutSec := input.GatewayRequestTimeoutSec + requestTimeoutSec := int(input.GatewayRequestTimeoutSec) if requestTimeoutSec == 0 { requestTimeoutSec = defaultGatewayRequestTimeoutSec } @@ -181,7 +181,7 @@ func buildServiceCentricJob(deps ProposeGatewayJobDeps, input ProposeGatewayJobI DONs: dons, Services: services, RequestTimeoutSec: requestTimeoutSec, - AllowedPorts: input.AllowedPorts, + AllowedPorts: toIntSlice(input.AllowedPorts), AllowedSchemes: input.AllowedSchemes, AllowedIPsCIDR: input.AllowedIPsCIDR, AuthGatewayID: input.AuthGatewayID, @@ -197,7 +197,7 @@ func buildLegacyFormatJob(deps ProposeGatewayJobDeps, input ProposeGatewayJobInp } targetDONs = append(targetDONs, pkg.TargetDON{ ID: ad.Name, - F: ad.F, + F: int(ad.F), Members: members, Handlers: ad.Handlers, }) @@ -207,7 +207,7 @@ func buildLegacyFormatJob(deps ProposeGatewayJobDeps, input ProposeGatewayJobInp JobName: "CRE Gateway", TargetDONs: targetDONs, RequestTimeoutSec: requestTimeoutSec, - AllowedPorts: input.AllowedPorts, + AllowedPorts: toIntSlice(input.AllowedPorts), AllowedSchemes: input.AllowedSchemes, AllowedIPsCIDR: input.AllowedIPsCIDR, AuthGatewayID: input.AuthGatewayID, @@ -288,6 +288,14 @@ func resolveDONMembers(deps ProposeGatewayJobDeps, input ProposeGatewayJobInput, return members, f, nil } +func toIntSlice(vs []pkg.Int) []int { + out := make([]int, len(vs)) + for i, v := range vs { + out[i] = int(v) + } + return out +} + func parseSelector(sel uint64) (nodev1.ChainType, string, error) { fam, err := chainsel.GetSelectorFamily(sel) if err != nil { diff --git a/deployment/cre/jobs/operations/propose_gateway_job_test.go b/deployment/cre/jobs/operations/propose_gateway_job_test.go index f363b6a23a2..52554f060ba 100644 --- a/deployment/cre/jobs/operations/propose_gateway_job_test.go +++ b/deployment/cre/jobs/operations/propose_gateway_job_test.go @@ -185,7 +185,7 @@ func TestProposeGatewayJob(t *testing.T) { output: ProposeGatewayJobOutput{ Specs: map[string][]string{ "node_5": { - "type = 'gateway'\nschemaVersion = 1\nname = 'CRE Gateway'\nexternalJobID = 'cf8aa339-6349-5e5b-9289-5c2907711200'\nforwardingAllowed = false\n\n[gatewayConfig]\n[gatewayConfig.ConnectionManagerConfig]\nAuthChallengeLen = 10\nAuthGatewayId = 'gateway-node-0'\nAuthTimestampToleranceSec = 5\nHeartbeatIntervalSec = 20\n\n[[gatewayConfig.ShardedDONs]]\nDonName = 'workflow_1_zone-b'\nF = 0\n\n[[gatewayConfig.ShardedDONs.Shards]]\n[[gatewayConfig.ShardedDONs.Shards.Nodes]]\nAddress = '0x04'\nName = 'cl-cre-one-zone-b-0 (DON workflow_1_zone-b)'\n\n[[gatewayConfig.Services]]\nServiceName = 'workflows'\nDONs = ['workflow_1_zone-b']\n\n[[gatewayConfig.Services.Handlers]]\nName = 'http-capabilities'\nServiceName = 'workflows'\n\n[gatewayConfig.Services.Handlers.Config]\nCleanUpPeriodMs = 600000\n\n[[gatewayConfig.Services.Handlers]]\nName = 'web-api-capabilities'\n\n[gatewayConfig.Services.Handlers.Config]\nmaxAllowedMessageAgeSec = 1000\n\n[gatewayConfig.Services.Handlers.Config.NodeRateLimiter]\nglobalBurst = 10\nglobalRPS = 50\nperSenderBurst = 10\nperSenderRPS = 10\n\n[gatewayConfig.HTTPClientConfig]\nMaxResponseBytes = 50000000\nAllowedPorts = [443]\nAllowedSchemes = ['https']\nAllowedIPsCIDR = []\n\n[gatewayConfig.NodeServerConfig]\nHandshakeTimeoutMillis = 1000\nMaxRequestBytes = 100000\nPath = '/'\nPort = 5003\nReadTimeoutMillis = 1000\nRequestTimeoutMillis = 5000\nWriteTimeoutMillis = 1000\n\n[gatewayConfig.UserServerConfig]\nContentTypeHeader = 'application/jsonrpc'\nMaxRequestBytes = 100000\nPath = '/'\nPort = 5002\nReadTimeoutMillis = 5000\nRequestTimeoutMillis = 5000\nWriteTimeoutMillis = 6000\n", + "type = 'gateway'\nschemaVersion = 1\nname = 'CRE Gateway'\nexternalJobID = 'cf8aa339-6349-5e5b-9289-5c2907711200'\nforwardingAllowed = false\n\n[gatewayConfig]\n[gatewayConfig.ConnectionManagerConfig]\nAuthChallengeLen = 10\nAuthGatewayId = 'gateway-node-0'\nAuthTimestampToleranceSec = 5\nHeartbeatIntervalSec = 20\n\n[[gatewayConfig.ShardedDONs]]\nDonName = 'workflow_1_zone-b'\nF = 0\n\n[[gatewayConfig.ShardedDONs.Shards]]\n[[gatewayConfig.ShardedDONs.Shards.Nodes]]\nAddress = '0x04'\nName = 'cl-cre-one-zone-b-0 (DON workflow_1_zone-b)'\n\n[[gatewayConfig.Services]]\nServiceName = 'workflows'\nDONs = ['workflow_1_zone-b']\n\n[[gatewayConfig.Services.Handlers]]\nName = 'http-capabilities'\nServiceName = 'workflows'\n\n[gatewayConfig.Services.Handlers.Config]\nCleanUpPeriodMs = 600000\n\n[gatewayConfig.Services.Handlers.Config.NodeRateLimiter]\nglobalBurst = 100\nglobalRPS = 500\nperSenderBurst = 100\nperSenderRPS = 100\n\n[[gatewayConfig.Services.Handlers]]\nName = 'web-api-capabilities'\n\n[gatewayConfig.Services.Handlers.Config]\nmaxAllowedMessageAgeSec = 1000\n\n[gatewayConfig.Services.Handlers.Config.NodeRateLimiter]\nglobalBurst = 10\nglobalRPS = 50\nperSenderBurst = 10\nperSenderRPS = 10\n\n[gatewayConfig.HTTPClientConfig]\nMaxResponseBytes = 50000000\nAllowedPorts = [443]\nAllowedSchemes = ['https']\nAllowedIPsCIDR = []\n\n[gatewayConfig.NodeServerConfig]\nHandshakeTimeoutMillis = 1000\nMaxRequestBytes = 100000\nPath = '/'\nPort = 5003\nReadTimeoutMillis = 1000\nRequestTimeoutMillis = 5000\nWriteTimeoutMillis = 1000\n\n[gatewayConfig.UserServerConfig]\nContentTypeHeader = 'application/jsonrpc'\nMaxRequestBytes = 100000\nPath = '/'\nPort = 5002\nReadTimeoutMillis = 5000\nRequestTimeoutMillis = 5000\nWriteTimeoutMillis = 6000\n", }, }, }, @@ -201,7 +201,7 @@ func TestProposeGatewayJob(t *testing.T) { output: ProposeGatewayJobOutput{ Specs: map[string][]string{ "node_5": { - "type = 'gateway'\nschemaVersion = 1\nname = 'CRE Gateway'\nexternalJobID = 'cf8aa339-6349-5e5b-9289-5c2907711200'\nforwardingAllowed = false\n\n[gatewayConfig]\n[gatewayConfig.ConnectionManagerConfig]\nAuthChallengeLen = 10\nAuthGatewayId = 'gateway-node-0'\nAuthTimestampToleranceSec = 5\nHeartbeatIntervalSec = 20\n\n[[gatewayConfig.Dons]]\nDonId = 'workflow_1_zone-b'\nF = 1\n\n[[gatewayConfig.Dons.Handlers]]\nName = 'http-capabilities'\nServiceName = 'workflows'\n\n[gatewayConfig.Dons.Handlers.Config]\nCleanUpPeriodMs = 600000\n\n[[gatewayConfig.Dons.Handlers]]\nName = 'web-api-capabilities'\n\n[gatewayConfig.Dons.Handlers.Config]\nmaxAllowedMessageAgeSec = 1000\n\n[gatewayConfig.Dons.Handlers.Config.NodeRateLimiter]\nglobalBurst = 10\nglobalRPS = 50\nperSenderBurst = 10\nperSenderRPS = 10\n\n[[gatewayConfig.Dons.Members]]\nAddress = '0x04'\nName = 'cl-cre-one-zone-b-0 (DON workflow_1_zone-b)'\n\n[gatewayConfig.HTTPClientConfig]\nMaxResponseBytes = 50000000\nAllowedPorts = [443]\nAllowedSchemes = ['https']\nAllowedIPsCIDR = []\n\n[gatewayConfig.NodeServerConfig]\nHandshakeTimeoutMillis = 1000\nMaxRequestBytes = 100000\nPath = '/'\nPort = 5003\nReadTimeoutMillis = 1000\nRequestTimeoutMillis = 5000\nWriteTimeoutMillis = 1000\n\n[gatewayConfig.UserServerConfig]\nContentTypeHeader = 'application/jsonrpc'\nMaxRequestBytes = 100000\nPath = '/'\nPort = 5002\nReadTimeoutMillis = 5000\nRequestTimeoutMillis = 5000\nWriteTimeoutMillis = 6000\n", + "type = 'gateway'\nschemaVersion = 1\nname = 'CRE Gateway'\nexternalJobID = 'cf8aa339-6349-5e5b-9289-5c2907711200'\nforwardingAllowed = false\n\n[gatewayConfig]\n[gatewayConfig.ConnectionManagerConfig]\nAuthChallengeLen = 10\nAuthGatewayId = 'gateway-node-0'\nAuthTimestampToleranceSec = 5\nHeartbeatIntervalSec = 20\n\n[[gatewayConfig.Dons]]\nDonId = 'workflow_1_zone-b'\nF = 1\n\n[[gatewayConfig.Dons.Handlers]]\nName = 'http-capabilities'\nServiceName = 'workflows'\n\n[gatewayConfig.Dons.Handlers.Config]\nCleanUpPeriodMs = 600000\n\n[gatewayConfig.Dons.Handlers.Config.NodeRateLimiter]\nglobalBurst = 100\nglobalRPS = 500\nperSenderBurst = 100\nperSenderRPS = 100\n\n[[gatewayConfig.Dons.Handlers]]\nName = 'web-api-capabilities'\n\n[gatewayConfig.Dons.Handlers.Config]\nmaxAllowedMessageAgeSec = 1000\n\n[gatewayConfig.Dons.Handlers.Config.NodeRateLimiter]\nglobalBurst = 10\nglobalRPS = 50\nperSenderBurst = 10\nperSenderRPS = 10\n\n[[gatewayConfig.Dons.Members]]\nAddress = '0x04'\nName = 'cl-cre-one-zone-b-0 (DON workflow_1_zone-b)'\n\n[gatewayConfig.HTTPClientConfig]\nMaxResponseBytes = 50000000\nAllowedPorts = [443]\nAllowedSchemes = ['https']\nAllowedIPsCIDR = []\n\n[gatewayConfig.NodeServerConfig]\nHandshakeTimeoutMillis = 1000\nMaxRequestBytes = 100000\nPath = '/'\nPort = 5003\nReadTimeoutMillis = 1000\nRequestTimeoutMillis = 5000\nWriteTimeoutMillis = 1000\n\n[gatewayConfig.UserServerConfig]\nContentTypeHeader = 'application/jsonrpc'\nMaxRequestBytes = 100000\nPath = '/'\nPort = 5002\nReadTimeoutMillis = 5000\nRequestTimeoutMillis = 5000\nWriteTimeoutMillis = 6000\n", }, }, }, diff --git a/deployment/cre/jobs/pkg/gateway_job.go b/deployment/cre/jobs/pkg/gateway_job.go index 2315f13afa7..950ebc79b41 100644 --- a/deployment/cre/jobs/pkg/gateway_job.go +++ b/deployment/cre/jobs/pkg/gateway_job.go @@ -425,7 +425,8 @@ type nodeRateLimiterConfig struct { } type httpCapabilitiesHandlerConfig struct { - CleanUpPeriodMs int `toml:"CleanUpPeriodMs"` + CleanUpPeriodMs int `toml:"CleanUpPeriodMs"` + NodeRateLimiter nodeRateLimiterConfig `toml:"NodeRateLimiter"` } func newDefaultHTTPCapabilitiesHandler() handler { @@ -434,6 +435,12 @@ func newDefaultHTTPCapabilitiesHandler() handler { ServiceName: "workflows", Config: httpCapabilitiesHandlerConfig{ CleanUpPeriodMs: 10 * 60 * 1000, // 10 minutes + NodeRateLimiter: nodeRateLimiterConfig{ + GlobalBurst: 100, + GlobalRPS: 500, + PerSenderBurst: 100, + PerSenderRPS: 100, + }, }, } } diff --git a/deployment/cre/jobs/pkg/gateway_job_test.go b/deployment/cre/jobs/pkg/gateway_job_test.go index f296e480af7..90d438284b3 100644 --- a/deployment/cre/jobs/pkg/gateway_job_test.go +++ b/deployment/cre/jobs/pkg/gateway_job_test.go @@ -290,6 +290,12 @@ ServiceName = 'workflows' [gatewayConfig.Services.Handlers.Config] CleanUpPeriodMs = 600000 +[gatewayConfig.Services.Handlers.Config.NodeRateLimiter] +globalBurst = 100 +globalRPS = 500 +perSenderBurst = 100 +perSenderRPS = 100 + [[gatewayConfig.Services]] ServiceName = 'vault' DONs = ['workflow_2'] diff --git a/deployment/cre/jobs/pkg/nodes.go b/deployment/cre/jobs/pkg/nodes.go index d397ab54cfc..4166dadc538 100644 --- a/deployment/cre/jobs/pkg/nodes.go +++ b/deployment/cre/jobs/pkg/nodes.go @@ -2,6 +2,7 @@ package pkg import ( "context" + "errors" "fmt" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" @@ -25,6 +26,9 @@ type FetchNodeChainConfigsResponse struct { } func FetchNodeChainConfigsFromJD(ctx context.Context, e cldf.Environment, req FetchNodesRequest) ([]FetchNodeChainConfigsResponse, error) { + if e.Offchain == nil { + return nil, errors.New("offchain client (JD) is not initialized; ensure JD_GRPC or OFFCHAIN_JD_ENDPOINTS_GRPC is set") + } resp, err := e.Offchain.ListNodes(ctx, &nodev1.ListNodesRequest{Filter: req.Filters}) if err != nil { return nil, fmt.Errorf("failed to list nodes: %w", err) diff --git a/deployment/cre/jobs/pkg/types.go b/deployment/cre/jobs/pkg/types.go index adea06101db..6fefc0f3884 100644 --- a/deployment/cre/jobs/pkg/types.go +++ b/deployment/cre/jobs/pkg/types.go @@ -29,6 +29,23 @@ type OracleFactoryConfig struct { Network string `toml:"network"` // e.g., "evm" } +// Int wraps int so that YAML fields can be populated from either a numeric +// literal or a quoted string (e.g. after environment-variable substitution). +type Int int + +func (i *Int) UnmarshalYAML(node *yaml.Node) error { + v, err := strconv.Atoi(node.Value) + if err != nil { + return err + } + *i = Int(v) + return nil +} + +func (i Int) MarshalYAML() ([]byte, error) { + return []byte(strconv.Itoa(int(i))), nil +} + type ChainSelector uint64 func (cs *ChainSelector) UnmarshalText(data []byte) error { diff --git a/deployment/cre/jobs/propose_job_spec_test.go b/deployment/cre/jobs/propose_job_spec_test.go index 6fde427d25d..37b7a97e51a 100644 --- a/deployment/cre/jobs/propose_job_spec_test.go +++ b/deployment/cre/jobs/propose_job_spec_test.go @@ -11,12 +11,14 @@ import ( "github.com/pelletier/go-toml/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldpipelineinput "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/pipeline/input" "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" "github.com/smartcontractkit/chainlink/deployment/cre/jobs" @@ -1753,3 +1755,132 @@ CallLimit = 1_000`, "invalid inputs for CRE settings job spec: invalid wf abcd: }) } } + +func TestProposeJobSpec_GatewayJobYAMLConversion(t *testing.T) { + t.Parallel() + + t.Run("service-centric format", func(t *testing.T) { + t.Parallel() + + yamlSpec := ` +environment: staging +domain: cre +changesets: + - job_propose_arbitrary: + payload: + donName: gateway-don + donFilters: + - key: don_name + value: gateway-don + - key: environment + value: staging + - key: product + value: cre + jobName: test-gateway-job-svc + template: gateway + inputs: + serviceCentricFormatEnabled: true + dons: + - name: workflow-don + f: 1 + services: + - servicename: workflows + handlers: + - web-api-capabilities + - http-capabilities + dons: + - workflow-don + gatewayRequestTimeoutSec: 10 + allowedSchemes: + - https + allowedIPsCIDR: + - 10.0.0.0/8 +` + var root yaml.Node + err := yaml.Unmarshal([]byte(yamlSpec), &root) + require.NoError(t, err) + + rootMap, ok := cldpipelineinput.YamlNodeToAny(&root).(map[string]any) + require.True(t, ok) + + environment, _ := rootMap["environment"].(string) + domain, _ := rootMap["domain"].(string) + + changesetData, err := cldpipelineinput.FindChangesetInData(rootMap["changesets"], "job_propose_arbitrary", "test") + require.NoError(t, err) + + changesetMap, ok := changesetData.(map[string]any) + require.True(t, ok) + + payload, ok := changesetMap["payload"] + require.True(t, ok) + + payloadBytes, err := yaml.Marshal(payload) + require.NoError(t, err) + + var parsed jobs.ProposeJobSpecInput + err = yaml.Unmarshal(payloadBytes, &parsed) + require.NoError(t, err) + + parsed.Environment = environment + parsed.Domain = domain + + assert.Equal(t, "staging", parsed.Environment) + assert.Equal(t, "cre", parsed.Domain) + assert.Equal(t, job_types.Gateway, parsed.Template) + + var gatewayInput operations.ProposeGatewayJobInput + err = parsed.Inputs.UnmarshalTo(&gatewayInput) + require.NoError(t, err) + + assert.True(t, gatewayInput.ServiceCentricFormatEnabled) + require.Len(t, gatewayInput.DONs, 1) + assert.Equal(t, "workflow-don", gatewayInput.DONs[0].Name) + assert.Equal(t, pkg.Int(1), gatewayInput.DONs[0].F) + require.Len(t, gatewayInput.Services, 1) + assert.Equal(t, "workflows", gatewayInput.Services[0].ServiceName) + assert.Equal(t, []string{"web-api-capabilities", "http-capabilities"}, gatewayInput.Services[0].Handlers) + assert.Equal(t, []string{"workflow-don"}, gatewayInput.Services[0].DONs) + assert.Equal(t, pkg.Int(10), gatewayInput.GatewayRequestTimeoutSec) + assert.Equal(t, []string{"https"}, gatewayInput.AllowedSchemes) + assert.Equal(t, []string{"10.0.0.0/8"}, gatewayInput.AllowedIPsCIDR) + + // Build GatewayJob manually; in production member addresses are resolved via JD. + gj := pkg.GatewayJob{ + ServiceCentricFormatEnabled: true, + JobName: "CRE Gateway", + DONs: []pkg.TargetDON{ + { + ID: gatewayInput.DONs[0].Name, + F: int(gatewayInput.DONs[0].F), + Members: []pkg.TargetDONMember{ + {Address: "0xdef456", Name: "mock-node-1 (DON workflow-don)"}, + }, + }, + }, + Services: []pkg.GatewayServiceConfig{ + { + ServiceName: gatewayInput.Services[0].ServiceName, + Handlers: gatewayInput.Services[0].Handlers, + DONs: gatewayInput.Services[0].DONs, + }, + }, + RequestTimeoutSec: int(gatewayInput.GatewayRequestTimeoutSec), + AllowedSchemes: gatewayInput.AllowedSchemes, + AllowedIPsCIDR: gatewayInput.AllowedIPsCIDR, + } + + require.NoError(t, gj.Validate()) + assert.True(t, gj.ServiceCentricFormatEnabled) + assert.Equal(t, "CRE Gateway", gj.JobName) + assert.Equal(t, 10, gj.RequestTimeoutSec) + assert.Equal(t, []string{"https"}, gj.AllowedSchemes) + assert.Equal(t, []string{"10.0.0.0/8"}, gj.AllowedIPsCIDR) + require.Len(t, gj.DONs, 1) + assert.Equal(t, "workflow-don", gj.DONs[0].ID) + require.Len(t, gj.Services, 1) + assert.Equal(t, "workflows", gj.Services[0].ServiceName) + assert.Equal(t, []string{"web-api-capabilities", "http-capabilities"}, gj.Services[0].Handlers) + assert.Equal(t, []string{"workflow-don"}, gj.Services[0].DONs) + }) +} diff --git a/deployment/cre/pkg/offchain/nodes.go b/deployment/cre/pkg/offchain/nodes.go index 8b7f98eb068..3b385a29f0b 100644 --- a/deployment/cre/pkg/offchain/nodes.go +++ b/deployment/cre/pkg/offchain/nodes.go @@ -2,6 +2,7 @@ package offchain import ( "context" + "errors" "fmt" "slices" "sort" @@ -32,6 +33,9 @@ const ( ) func FetchNodesFromJD(ctx context.Context, jd cldf_offchain.Client, filter *nodeapiv1.ListNodesRequest_Filter) (nodes []*nodeapiv1.Node, err error) { + if jd == nil { + return nil, errors.New("offchain client (JD) is not initialized; ensure JD_GRPC or OFFCHAIN_JD_ENDPOINTS_GRPC is set") + } resp, err := jd.ListNodes(ctx, &nodeapiv1.ListNodesRequest{Filter: filter}) if err != nil { return nil, fmt.Errorf("failed to list nodes: %w", err) diff --git a/deployment/go.mod b/deployment/go.mod index 3025eff495a..5771670054d 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -44,17 +44,17 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260224214816-cb23ec38649f github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317175207-e9ff89561326 - github.com/smartcontractkit/chainlink-common v0.11.0 + github.com/smartcontractkit/chainlink-common v0.11.1 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260320152158-2191d797b5ce github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260107191744-4b93f62cffe3 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 - github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 + github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075 github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0 @@ -435,7 +435,7 @@ require ( github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706 // indirect github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 // indirect github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect @@ -454,6 +454,7 @@ require ( github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/supranational/blst v0.3.16 // indirect + github.com/suzuki-shunsuke/go-convmap v0.2.1 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect github.com/theodesp/go-heaps v0.0.0-20190520121037-88e35354fe0a // indirect diff --git a/deployment/go.sum b/deployment/go.sum index 5107e164ee0..799a9671968 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1387,8 +1387,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317175207-e9ff github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317175207-e9ff89561326/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.0 h1:b/6fGMruUCKqxxzNBmTjCupRkd+m6LqvPCBBMTkpxU0= -github.com/smartcontractkit/chainlink-common v0.11.0/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.11.1 h1:JVTnqoQjdLDmQQXNgssmzEQnJK0gQ/0427LqS4UDuqE= +github.com/smartcontractkit/chainlink-common v0.11.1/go.mod h1:9W8E7tfchAsrSNHdMM1mzLmle+bL1P8Ou0I4LG1qNxw= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1425,14 +1425,14 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0. github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:ATjAPIVJibHRcIfiG47rEQkUIOoYa6KDvWj3zwCAw6g= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d h1:AJy55QJ/pBhXkZjc7N+ATnWfxrcjq9BI9DmdtdjwDUQ= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 h1:03tbcwjyIEjvHba1IWOj1sfThwebm2XNzyFHSuZtlWc= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 h1:fkS5FJpSozwxL2FA6OJDi7az2DrtMNiK1X5DWuHDyfA= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 h1:hhevsu8k7tlDRrYZmgAh7V4avGQDMvus1bwIlial3Ps= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 h1:oli+2uLU6jcrJGCuYFqk3475hiwL17SWlITWLv+tx/w= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 h1:AEnxv4HM3WD1RbQkRiFyb9cJ6YKAcqBp1CpIcFdZfuo= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= @@ -1447,8 +1447,8 @@ github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f h1:3+vQMwuWL6+OqNutFqo/+gkczJwcr+MBPqeSxcjfI1Y= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 h1:XLMJ6FDQoEiqDNZ4B1MV9Vi1lL8vOfo9SzgqkM8IiuA= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c h1:7MUil5RQBxxnmfwp2bc1N4jv/8FVLH0hAkJupnGNMCg= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075 h1:PcR7Zdh+Z+Dh/S4lQ1xDbnFrb6He70KW9O5+9DtgloE= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075/go.mod h1:APCV5fIW/a+JGM+Cz9yb6XyGt8ht5hISEYfpG/k4Z+k= github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1:4mGJySR1GAJAAFRwEo6YiSKM2zSHzYT5b/FSmrpNUGI= @@ -1560,6 +1560,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/suzuki-shunsuke/go-convmap v0.2.1 h1:g94CxI6ENYluXZhdEH+1WVGhMAE8nLvAmWLUCwBw6W0= +github.com/suzuki-shunsuke/go-convmap v0.2.1/go.mod h1:3XfGRbtyNBMGfXAxhROSRki6/UIlUX31Qt6DvdI6lUs= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= diff --git a/devenv/contracts/vrf_v2.go b/devenv/contracts/vrf_v2.go new file mode 100644 index 00000000000..463897f20a0 --- /dev/null +++ b/devenv/contracts/vrf_v2.go @@ -0,0 +1,699 @@ +package contracts + +import ( + "context" + "errors" + "fmt" + "math/big" + "strconv" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/shopspring/decimal" + + "github.com/smartcontractkit/chainlink-testing-framework/seth" + + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/batch_vrf_coordinator_v2" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/vrf_coordinator_v2" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/vrf_load_test_with_metrics" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/vrfv2_wrapper" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/vrfv2_wrapper_load_test_consumer" +) + +// CoordinatorV2Log is implemented by EthereumVRFCoordinatorV2 for log parsing and waits. +type CoordinatorV2Log interface { + ParseRandomWordsRequested(log types.Log) (*CoordinatorRandomWordsRequested, error) + ParseRandomWordsFulfilled(log types.Log) (*CoordinatorRandomWordsFulfilled, error) + Address() string + WaitForRandomWordsFulfilledEvent(filter RandomWordsFulfilledEventFilter) (*CoordinatorRandomWordsFulfilled, error) + WaitForConfigSetEvent(timeout time.Duration) (*CoordinatorConfigSet, error) + FilterRandomWordsFulfilledEvent(opts *bind.FilterOpts, requestID *big.Int) (*CoordinatorRandomWordsFulfilled, error) +} + +// CoordinatorRandomWordsFulfilled mirrors integration-tests coordinator fulfilled shape. +type CoordinatorRandomWordsFulfilled struct { + RequestID *big.Int + OutputSeed *big.Int + SubID string + Payment *big.Int + NativePayment bool + Success bool + OnlyPremium bool + Raw types.Log +} + +// CoordinatorRandomWordsRequested mirrors integration-tests coordinator requested shape. +type CoordinatorRandomWordsRequested struct { + KeyHash [32]byte + RequestID *big.Int + PreSeed *big.Int + SubID string + MinimumRequestConfirmations uint16 + CallbackGasLimit uint32 + NumWords uint32 + ExtraArgs []byte + Sender common.Address + Raw types.Log +} + +// CoordinatorConfigSet is emitted when coordinator config changes. +type CoordinatorConfigSet struct { + MinimumRequestConfirmations uint16 + MaxGasLimit uint32 + StalenessSeconds uint32 + GasAfterPaymentCalculation uint32 + FallbackWeiPerUnitLink *big.Int + FulfillmentFlatFeeNativePPM uint32 + FulfillmentFlatFeeLinkDiscountPPM uint32 + NativePremiumPercentage uint8 + LinkPremiumPercentage uint8 + FeeConfig VRFCoordinatorV2OnChainFeeConfig + Raw types.Log +} + +// VRFCoordinatorV2OnChainFeeConfig is the coordinator fee config struct. +type VRFCoordinatorV2OnChainFeeConfig struct { + FulfillmentFlatFeeLinkPPMTier1 uint32 + FulfillmentFlatFeeLinkPPMTier2 uint32 + FulfillmentFlatFeeLinkPPMTier3 uint32 + FulfillmentFlatFeeLinkPPMTier4 uint32 + FulfillmentFlatFeeLinkPPMTier5 uint32 + ReqsForTier2 *big.Int + ReqsForTier3 *big.Int + ReqsForTier4 *big.Int + ReqsForTier5 *big.Int +} + +// RandomWordsFulfilledEventFilter filters fulfilled events. +type RandomWordsFulfilledEventFilter struct { + RequestIDs []*big.Int + SubIDs []*big.Int + Timeout time.Duration +} + +// VRFv2Subscription is returned by GetSubscription on the V2 coordinator. +type VRFv2Subscription struct { + Balance *big.Int + NativeBalance *big.Int + ReqCount uint64 + SubOwner common.Address + Consumers []common.Address +} + +// VRFLoadTestMetrics holds consumer load-test counters. +type VRFLoadTestMetrics struct { + RequestCount *big.Int + FulfilmentCount *big.Int + AverageFulfillmentInMillions *big.Int + SlowestFulfillment *big.Int + FastestFulfillment *big.Int +} + +// --- Coordinator V2 --- + +// EthereumVRFCoordinatorV2 wraps vrf_coordinator_v2.VRFCoordinatorV2. +type EthereumVRFCoordinatorV2 struct { + client *seth.Client + coordinator *vrf_coordinator_v2.VRFCoordinatorV2 + address common.Address +} + +func (v *EthereumVRFCoordinatorV2) Address() string { return v.address.Hex() } + +func (v *EthereumVRFCoordinatorV2) Coordinator() *vrf_coordinator_v2.VRFCoordinatorV2 { + return v.coordinator +} + +// DeployVRFCoordinatorV2 deploys the VRF Coordinator V2 contract. +func DeployVRFCoordinatorV2(client *seth.Client, linkAddr, bhsAddr, linkEthFeedAddr string) (*EthereumVRFCoordinatorV2, error) { + abi, err := vrf_coordinator_v2.VRFCoordinatorV2MetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("failed to get VRFCoordinatorV2 ABI: %w", err) + } + data, err := client.DeployContract(client.NewTXOpts(), "VRFCoordinatorV2", *abi, + common.FromHex(vrf_coordinator_v2.VRFCoordinatorV2MetaData.Bin), + common.HexToAddress(linkAddr), + common.HexToAddress(bhsAddr), + common.HexToAddress(linkEthFeedAddr)) + if err != nil { + return nil, fmt.Errorf("VRFCoordinatorV2 deployment failed: %w", err) + } + instance, err := vrf_coordinator_v2.NewVRFCoordinatorV2(data.Address, MustNewWrappedContractBackend(nil, client)) + if err != nil { + return nil, fmt.Errorf("failed to instantiate VRFCoordinatorV2: %w", err) + } + return &EthereumVRFCoordinatorV2{client: client, coordinator: instance, address: data.Address}, nil +} + +// LoadVRFCoordinatorV2 binds an existing VRF Coordinator V2. +func LoadVRFCoordinatorV2(client *seth.Client, addr string) (*EthereumVRFCoordinatorV2, error) { + address := common.HexToAddress(addr) + abi, err := vrf_coordinator_v2.VRFCoordinatorV2MetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("failed to get VRFCoordinatorV2 ABI: %w", err) + } + client.ContractStore.AddABI("VRFCoordinatorV2", *abi) + client.ContractStore.AddBIN("VRFCoordinatorV2", common.FromHex(vrf_coordinator_v2.VRFCoordinatorV2MetaData.Bin)) + instance, err := vrf_coordinator_v2.NewVRFCoordinatorV2(address, MustNewWrappedContractBackend(nil, client)) + if err != nil { + return nil, fmt.Errorf("failed to load VRFCoordinatorV2: %w", err) + } + return &EthereumVRFCoordinatorV2{client: client, coordinator: instance, address: address}, nil +} + +func (v *EthereumVRFCoordinatorV2) HashOfKey(ctx context.Context, pubKey [2]*big.Int) ([32]byte, error) { + return v.coordinator.HashOfKey(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}, pubKey) +} + +func (v *EthereumVRFCoordinatorV2) GetSubscription(ctx context.Context, subID uint64) (VRFv2Subscription, error) { + sub, err := v.coordinator.GetSubscription(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}, subID) + if err != nil { + return VRFv2Subscription{}, err + } + return VRFv2Subscription{ + Balance: sub.Balance, + NativeBalance: nil, + ReqCount: sub.ReqCount, + SubOwner: sub.Owner, + Consumers: sub.Consumers, + }, nil +} + +func (v *EthereumVRFCoordinatorV2) SetConfig( + minimumRequestConfirmations uint16, + maxGasLimit uint32, + stalenessSeconds uint32, + gasAfterPaymentCalculation uint32, + fallbackWeiPerUnitLink *big.Int, + feeConfig vrf_coordinator_v2.VRFCoordinatorV2FeeConfig, +) error { + _, err := v.client.Decode(v.coordinator.SetConfig( + v.client.NewTXOpts(), + minimumRequestConfirmations, + maxGasLimit, + stalenessSeconds, + gasAfterPaymentCalculation, + fallbackWeiPerUnitLink, + feeConfig, + )) + return err +} + +func (v *EthereumVRFCoordinatorV2) RegisterProvingKey(oracleAddr string, publicProvingKey [2]*big.Int) error { + _, err := v.client.Decode(v.coordinator.RegisterProvingKey( + v.client.NewTXOpts(), + common.HexToAddress(oracleAddr), + publicProvingKey, + )) + return err +} + +func (v *EthereumVRFCoordinatorV2) CreateSubscription() (*types.Receipt, error) { + tx, err := v.client.Decode(v.coordinator.CreateSubscription(v.client.NewTXOpts())) + if err != nil { + return nil, err + } + return tx.Receipt, err +} + +func (v *EthereumVRFCoordinatorV2) AddConsumer(subID uint64, consumerAddress string) error { + _, err := v.client.Decode(v.coordinator.AddConsumer( + v.client.NewTXOpts(), + subID, + common.HexToAddress(consumerAddress), + )) + return err +} + +func (v *EthereumVRFCoordinatorV2) PendingRequestsExist(ctx context.Context, subID uint64) (bool, error) { + return v.coordinator.PendingRequestExists(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}, subID) +} + +func (v *EthereumVRFCoordinatorV2) OracleWithdraw(recipient common.Address, amount *big.Int) error { + _, err := v.client.Decode(v.coordinator.OracleWithdraw(v.client.NewTXOpts(), recipient, amount)) + return err +} + +func (v *EthereumVRFCoordinatorV2) CancelSubscription(subID uint64, to common.Address) (*seth.DecodedTransaction, *vrf_coordinator_v2.VRFCoordinatorV2SubscriptionCanceled, error) { + tx, err := v.client.Decode(v.coordinator.CancelSubscription(v.client.NewTXOpts(), subID, to)) + if err != nil { + return nil, nil, err + } + var canceled *vrf_coordinator_v2.VRFCoordinatorV2SubscriptionCanceled + for _, lg := range tx.Receipt.Logs { + for _, topic := range lg.Topics { + if topic.Cmp(vrf_coordinator_v2.VRFCoordinatorV2SubscriptionCanceled{}.Topic()) == 0 { + canceled, err = v.coordinator.ParseSubscriptionCanceled(*lg) + if err != nil { + return nil, nil, fmt.Errorf("parse SubscriptionCanceled: %w", err) + } + } + } + } + if canceled == nil { + return tx, nil, errors.New("no SubscriptionCanceled event in transaction receipt logs") + } + return tx, canceled, nil +} + +func (v *EthereumVRFCoordinatorV2) OwnerCancelSubscription(subID uint64) (*seth.DecodedTransaction, *vrf_coordinator_v2.VRFCoordinatorV2SubscriptionCanceled, error) { + tx, err := v.client.Decode(v.coordinator.OwnerCancelSubscription(v.client.NewTXOpts(), subID)) + if err != nil { + return nil, nil, err + } + var canceled *vrf_coordinator_v2.VRFCoordinatorV2SubscriptionCanceled + for _, lg := range tx.Receipt.Logs { + for _, topic := range lg.Topics { + if topic.Cmp(vrf_coordinator_v2.VRFCoordinatorV2SubscriptionCanceled{}.Topic()) == 0 { + canceled, err = v.coordinator.ParseSubscriptionCanceled(*lg) + if err != nil { + return nil, nil, fmt.Errorf("parse SubscriptionCanceled: %w", err) + } + } + } + } + if canceled == nil { + return tx, nil, errors.New("no SubscriptionCanceled event in transaction receipt logs") + } + return tx, canceled, nil +} + +func (v *EthereumVRFCoordinatorV2) GetConfig(ctx context.Context) (vrf_coordinator_v2.GetConfig, error) { + return v.coordinator.GetConfig(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}) +} + +func (v *EthereumVRFCoordinatorV2) GetFeeConfig(ctx context.Context) (vrf_coordinator_v2.GetFeeConfig, error) { + return v.coordinator.GetFeeConfig(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}) +} + +func (v *EthereumVRFCoordinatorV2) GetFallbackWeiPerUnitLink(ctx context.Context) (*big.Int, error) { + return v.coordinator.GetFallbackWeiPerUnitLink(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}) +} + +func (v *EthereumVRFCoordinatorV2) ParseRandomWordsRequested(log types.Log) (*CoordinatorRandomWordsRequested, error) { + ev, err := v.coordinator.ParseRandomWordsRequested(log) + if err != nil { + return nil, err + } + return &CoordinatorRandomWordsRequested{ + KeyHash: ev.KeyHash, + RequestID: ev.RequestId, + PreSeed: ev.PreSeed, + SubID: strconv.FormatUint(ev.SubId, 10), + MinimumRequestConfirmations: ev.MinimumRequestConfirmations, + CallbackGasLimit: ev.CallbackGasLimit, + NumWords: ev.NumWords, + Sender: ev.Sender, + Raw: ev.Raw, + }, nil +} + +func (v *EthereumVRFCoordinatorV2) ParseRandomWordsFulfilled(log types.Log) (*CoordinatorRandomWordsFulfilled, error) { + ev, err := v.coordinator.ParseRandomWordsFulfilled(log) + if err != nil { + return nil, err + } + return &CoordinatorRandomWordsFulfilled{ + RequestID: ev.RequestId, + OutputSeed: ev.OutputSeed, + Payment: ev.Payment, + Success: ev.Success, + Raw: ev.Raw, + }, nil +} + +func (v *EthereumVRFCoordinatorV2) FilterRandomWordsFulfilledEvent(opts *bind.FilterOpts, requestID *big.Int) (*CoordinatorRandomWordsFulfilled, error) { + it, err := v.coordinator.FilterRandomWordsFulfilled(opts, []*big.Int{requestID}) + if err != nil { + return nil, err + } + if !it.Next() { + return nil, fmt.Errorf("no RandomWordsFulfilled for request %s", requestID.String()) + } + ev := it.Event + return &CoordinatorRandomWordsFulfilled{ + RequestID: ev.RequestId, + OutputSeed: ev.OutputSeed, + Payment: ev.Payment, + Success: ev.Success, + Raw: ev.Raw, + }, nil +} + +func (v *EthereumVRFCoordinatorV2) WaitForRandomWordsFulfilledEvent(filter RandomWordsFulfilledEventFilter) (*CoordinatorRandomWordsFulfilled, error) { + ch := make(chan *vrf_coordinator_v2.VRFCoordinatorV2RandomWordsFulfilled) + sub, err := v.coordinator.WatchRandomWordsFulfilled(nil, ch, filter.RequestIDs) + if err != nil { + return nil, err + } + defer sub.Unsubscribe() + for { + select { + case err := <-sub.Err(): + return nil, err + case <-time.After(filter.Timeout): + return nil, errors.New("timeout waiting for RandomWordsFulfilled event") + case ev := <-ch: + return &CoordinatorRandomWordsFulfilled{ + RequestID: ev.RequestId, + OutputSeed: ev.OutputSeed, + Payment: ev.Payment, + Success: ev.Success, + Raw: ev.Raw, + }, nil + } + } +} + +func (v *EthereumVRFCoordinatorV2) WaitForConfigSetEvent(timeout time.Duration) (*CoordinatorConfigSet, error) { + ch := make(chan *vrf_coordinator_v2.VRFCoordinatorV2ConfigSet) + sub, err := v.coordinator.WatchConfigSet(nil, ch) + if err != nil { + return nil, err + } + defer sub.Unsubscribe() + for { + select { + case err := <-sub.Err(): + return nil, err + case <-time.After(timeout): + return nil, errors.New("timeout waiting for ConfigSet event") + case ev := <-ch: + return &CoordinatorConfigSet{ + MinimumRequestConfirmations: ev.MinimumRequestConfirmations, + MaxGasLimit: ev.MaxGasLimit, + StalenessSeconds: ev.StalenessSeconds, + GasAfterPaymentCalculation: ev.GasAfterPaymentCalculation, + FallbackWeiPerUnitLink: ev.FallbackWeiPerUnitLink, + FeeConfig: VRFCoordinatorV2OnChainFeeConfig{ + FulfillmentFlatFeeLinkPPMTier1: ev.FeeConfig.FulfillmentFlatFeeLinkPPMTier1, + FulfillmentFlatFeeLinkPPMTier2: ev.FeeConfig.FulfillmentFlatFeeLinkPPMTier2, + FulfillmentFlatFeeLinkPPMTier3: ev.FeeConfig.FulfillmentFlatFeeLinkPPMTier3, + FulfillmentFlatFeeLinkPPMTier4: ev.FeeConfig.FulfillmentFlatFeeLinkPPMTier4, + FulfillmentFlatFeeLinkPPMTier5: ev.FeeConfig.FulfillmentFlatFeeLinkPPMTier5, + ReqsForTier2: ev.FeeConfig.ReqsForTier2, + ReqsForTier3: ev.FeeConfig.ReqsForTier3, + ReqsForTier4: ev.FeeConfig.ReqsForTier4, + ReqsForTier5: ev.FeeConfig.ReqsForTier5, + }, + }, nil + } + } +} + +// ParseRandomWordsFulfilledLogs parses all RandomWordsFulfilled logs in a receipt. +func ParseRandomWordsFulfilledLogs(coordinator CoordinatorV2Log, logs []*types.Log) ([]*CoordinatorRandomWordsFulfilled, error) { + var out []*CoordinatorRandomWordsFulfilled + for _, lg := range logs { + for _, topic := range lg.Topics { + if topic.Cmp(vrf_coordinator_v2.VRFCoordinatorV2RandomWordsFulfilled{}.Topic()) == 0 { + ev, err := coordinator.ParseRandomWordsFulfilled(*lg) + if err != nil { + return nil, err + } + out = append(out, ev) + } + } + } + return out, nil +} + +func parseRequestRandomnessLogs(coordinator CoordinatorV2Log, logs []*types.Log) (*CoordinatorRandomWordsRequested, error) { + var requested *CoordinatorRandomWordsRequested + var err error + for _, lg := range logs { + for _, topic := range lg.Topics { + if topic.Cmp(vrf_coordinator_v2.VRFCoordinatorV2RandomWordsRequested{}.Topic()) == 0 { + requested, err = coordinator.ParseRandomWordsRequested(*lg) + if err != nil { + return nil, fmt.Errorf("parse RandomWordsRequested: %w", err) + } + } + } + } + if requested == nil { + return nil, errors.New("no RandomWordsRequested event in transaction receipt logs") + } + return requested, nil +} + +// FindVRFv2SubscriptionID returns the uint64 sub ID from a CreateSubscription receipt by locating SubscriptionCreated. +func FindVRFv2SubscriptionID(receipt *types.Receipt) (uint64, error) { + wantTopic := vrf_coordinator_v2.VRFCoordinatorV2SubscriptionCreated{}.Topic() + for _, lg := range receipt.Logs { + if len(lg.Topics) < 2 { + continue + } + if lg.Topics[0].Cmp(wantTopic) == 0 { + return lg.Topics[1].Big().Uint64(), nil + } + } + return 0, errors.New("no SubscriptionCreated event in transaction receipt logs") +} + +// FallbackWeiBigInt parses decimal string fallback wei per unit LINK. +func FallbackWeiBigInt(s string) (*big.Int, error) { + d, err := decimal.NewFromString(s) + if err != nil { + return nil, err + } + return d.BigInt(), nil +} + +// --- Batch coordinator V2 --- + +// EthereumBatchVRFCoordinatorV2 wraps batch_vrf_coordinator_v2. +type EthereumBatchVRFCoordinatorV2 struct { + client *seth.Client + batchCoordinator *batch_vrf_coordinator_v2.BatchVRFCoordinatorV2 + address common.Address +} + +func (v *EthereumBatchVRFCoordinatorV2) Address() string { return v.address.Hex() } + +// DeployBatchVRFCoordinatorV2 deploys the batch coordinator pointing at VRF Coordinator V2. +func DeployBatchVRFCoordinatorV2(client *seth.Client, coordinatorAddress string) (*EthereumBatchVRFCoordinatorV2, error) { + abi, err := batch_vrf_coordinator_v2.BatchVRFCoordinatorV2MetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("failed to get BatchVRFCoordinatorV2 ABI: %w", err) + } + data, err := client.DeployContract(client.NewTXOpts(), "BatchVRFCoordinatorV2", *abi, + common.FromHex(batch_vrf_coordinator_v2.BatchVRFCoordinatorV2MetaData.Bin), + common.HexToAddress(coordinatorAddress)) + if err != nil { + return nil, fmt.Errorf("BatchVRFCoordinatorV2 deployment failed: %w", err) + } + instance, err := batch_vrf_coordinator_v2.NewBatchVRFCoordinatorV2(data.Address, MustNewWrappedContractBackend(nil, client)) + if err != nil { + return nil, fmt.Errorf("failed to instantiate BatchVRFCoordinatorV2: %w", err) + } + return &EthereumBatchVRFCoordinatorV2{client: client, batchCoordinator: instance, address: data.Address}, nil +} + +// --- Load test consumer --- + +// EthereumVRFv2LoadTestConsumer wraps VRFV2LoadTestWithMetrics. +type EthereumVRFv2LoadTestConsumer struct { + client *seth.Client + consumer *vrf_load_test_with_metrics.VRFV2LoadTestWithMetrics + address common.Address +} + +func (v *EthereumVRFv2LoadTestConsumer) Address() string { return v.address.Hex() } + +// DeployVRFv2LoadTestConsumer deploys a VRF v2 load test consumer. +func DeployVRFv2LoadTestConsumer(client *seth.Client, coordinatorAddr string) (*EthereumVRFv2LoadTestConsumer, error) { + abi, err := vrf_load_test_with_metrics.VRFV2LoadTestWithMetricsMetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("failed to get VRFV2LoadTestWithMetrics ABI: %w", err) + } + data, err := client.DeployContract(client.NewTXOpts(), "VRFV2LoadTestWithMetrics", *abi, + common.FromHex(vrf_load_test_with_metrics.VRFV2LoadTestWithMetricsMetaData.Bin), + common.HexToAddress(coordinatorAddr)) + if err != nil { + return nil, fmt.Errorf("VRFV2LoadTestWithMetrics deployment failed: %w", err) + } + instance, err := vrf_load_test_with_metrics.NewVRFV2LoadTestWithMetrics(data.Address, MustNewWrappedContractBackend(nil, client)) + if err != nil { + return nil, fmt.Errorf("failed to instantiate VRFV2LoadTestWithMetrics: %w", err) + } + return &EthereumVRFv2LoadTestConsumer{client: client, consumer: instance, address: data.Address}, nil +} + +func (v *EthereumVRFv2LoadTestConsumer) RequestRandomnessFromKey( + coordinator CoordinatorV2Log, + keyHash [32]byte, + subID uint64, + requestConfirmations uint16, + callbackGasLimit uint32, + numWords uint32, + requestCount uint16, + keyNum int, +) (*CoordinatorRandomWordsRequested, error) { + tx, err := v.client.Decode(v.consumer.RequestRandomWords( + v.client.NewTXKeyOpts(keyNum), + subID, + requestConfirmations, + keyHash, + callbackGasLimit, + numWords, + requestCount, + )) // matches integration-tests RequestRandomWordsFromKey argument order + if err != nil { + return nil, fmt.Errorf("RequestRandomWords: %w", err) + } + return parseRequestRandomnessLogs(coordinator, tx.Receipt.Logs) +} + +func (v *EthereumVRFv2LoadTestConsumer) GetRequestStatus(ctx context.Context, requestID *big.Int) (vrf_load_test_with_metrics.GetRequestStatus, error) { + return v.consumer.GetRequestStatus(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}, requestID) +} + +func (v *EthereumVRFv2LoadTestConsumer) GetLoadTestMetrics(ctx context.Context) (*VRFLoadTestMetrics, error) { + reqCount, err := v.consumer.SRequestCount(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}) + if err != nil { + return nil, err + } + fulfillCount, err := v.consumer.SResponseCount(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}) + if err != nil { + return nil, err + } + avg, err := v.consumer.SAverageFulfillmentInMillions(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}) + if err != nil { + return nil, err + } + slow, err := v.consumer.SSlowestFulfillment(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}) + if err != nil { + return nil, err + } + fast, err := v.consumer.SFastestFulfillment(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}) + if err != nil { + return nil, err + } + return &VRFLoadTestMetrics{ + RequestCount: reqCount, + FulfilmentCount: fulfillCount, + AverageFulfillmentInMillions: avg, + SlowestFulfillment: slow, + FastestFulfillment: fast, + }, nil +} + +// WaitRandomWordsFulfilled waits for fulfillment using watch then filter fallback. +func WaitRandomWordsFulfilled( + coordinator *EthereumVRFCoordinatorV2, + requestID *big.Int, + requestBlock uint64, + timeout time.Duration, +) (*CoordinatorRandomWordsFulfilled, error) { + ev, err := coordinator.WaitForRandomWordsFulfilledEvent(RandomWordsFulfilledEventFilter{ + RequestIDs: []*big.Int{requestID}, + Timeout: timeout, + }) + if err == nil { + return ev, nil + } + return coordinator.FilterRandomWordsFulfilledEvent(&bind.FilterOpts{Start: requestBlock}, requestID) +} + +// --- VRF v2 wrapper (direct funding) --- + +// EthereumVRFV2Wrapper wraps vrfv2_wrapper.VRFV2Wrapper. +type EthereumVRFV2Wrapper struct { + client *seth.Client + wrapper *vrfv2_wrapper.VRFV2Wrapper + address common.Address +} + +func (v *EthereumVRFV2Wrapper) Address() string { return v.address.Hex() } + +// DeployVRFV2Wrapper deploys VRFV2Wrapper. +func DeployVRFV2Wrapper(client *seth.Client, linkAddr, linkEthFeedAddr, coordinatorAddr string) (*EthereumVRFV2Wrapper, error) { + abi, err := vrfv2_wrapper.VRFV2WrapperMetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("failed to get VRFV2Wrapper ABI: %w", err) + } + data, err := client.DeployContract(client.NewTXOpts(), "VRFV2Wrapper", *abi, + common.FromHex(vrfv2_wrapper.VRFV2WrapperMetaData.Bin), + common.HexToAddress(linkAddr), + common.HexToAddress(linkEthFeedAddr), + common.HexToAddress(coordinatorAddr)) + if err != nil { + return nil, fmt.Errorf("VRFV2Wrapper deployment failed: %w", err) + } + instance, err := vrfv2_wrapper.NewVRFV2Wrapper(data.Address, MustNewWrappedContractBackend(nil, client)) + if err != nil { + return nil, fmt.Errorf("failed to instantiate VRFV2Wrapper: %w", err) + } + return &EthereumVRFV2Wrapper{client: client, wrapper: instance, address: data.Address}, nil +} + +func (v *EthereumVRFV2Wrapper) SetConfig(wrapperGasOverhead, coordinatorGasOverhead uint32, wrapperPremiumPercentage uint8, keyHash [32]byte, maxNumWords uint8) error { + _, err := v.client.Decode(v.wrapper.SetConfig( + v.client.NewTXOpts(), + wrapperGasOverhead, + coordinatorGasOverhead, + wrapperPremiumPercentage, + keyHash, + maxNumWords, + )) + return err +} + +func (v *EthereumVRFV2Wrapper) GetSubID(ctx context.Context) (uint64, error) { + return v.wrapper.SUBSCRIPTIONID(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}) +} + +// EthereumVRFV2WrapperLoadTestConsumer wraps the wrapper load test consumer. +type EthereumVRFV2WrapperLoadTestConsumer struct { + client *seth.Client + consumer *vrfv2_wrapper_load_test_consumer.VRFV2WrapperLoadTestConsumer + address common.Address +} + +func (v *EthereumVRFV2WrapperLoadTestConsumer) Address() string { return v.address.Hex() } + +// DeployVRFV2WrapperLoadTestConsumer deploys wrapper load test consumer. +func DeployVRFV2WrapperLoadTestConsumer(client *seth.Client, linkAddr, wrapperAddr string) (*EthereumVRFV2WrapperLoadTestConsumer, error) { + abi, err := vrfv2_wrapper_load_test_consumer.VRFV2WrapperLoadTestConsumerMetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("failed to get VRFV2WrapperLoadTestConsumer ABI: %w", err) + } + data, err := client.DeployContract(client.NewTXOpts(), "VRFV2WrapperLoadTestConsumer", *abi, + common.FromHex(vrfv2_wrapper_load_test_consumer.VRFV2WrapperLoadTestConsumerMetaData.Bin), + common.HexToAddress(linkAddr), common.HexToAddress(wrapperAddr)) + if err != nil { + return nil, fmt.Errorf("VRFV2WrapperLoadTestConsumer deployment failed: %w", err) + } + instance, err := vrfv2_wrapper_load_test_consumer.NewVRFV2WrapperLoadTestConsumer(data.Address, MustNewWrappedContractBackend(nil, client)) + if err != nil { + return nil, fmt.Errorf("failed to instantiate VRFV2WrapperLoadTestConsumer: %w", err) + } + return &EthereumVRFV2WrapperLoadTestConsumer{client: client, consumer: instance, address: data.Address}, nil +} + +func (v *EthereumVRFV2WrapperLoadTestConsumer) RequestRandomness( + coordinator CoordinatorV2Log, + requestConfirmations uint16, + callbackGasLimit uint32, + numWords uint32, + requestCount uint16, +) (*CoordinatorRandomWordsRequested, error) { + tx, err := v.client.Decode(v.consumer.MakeRequests( + v.client.NewTXOpts(), + callbackGasLimit, + requestConfirmations, + numWords, + requestCount, + )) + if err != nil { + return nil, err + } + return parseRequestRandomnessLogs(coordinator, tx.Receipt.Logs) +} + +func (v *EthereumVRFV2WrapperLoadTestConsumer) GetRequestStatus(ctx context.Context, requestID *big.Int) (vrfv2_wrapper_load_test_consumer.GetRequestStatus, error) { + return v.consumer.GetRequestStatus(&bind.CallOpts{From: v.client.MustGetRootKeyAddress(), Context: ctx}, requestID) +} diff --git a/devenv/env-vrfv2-bhs.toml b/devenv/env-vrfv2-bhs.toml new file mode 100644 index 00000000000..1703fa1f519 --- /dev/null +++ b/devenv/env-vrfv2-bhs.toml @@ -0,0 +1,28 @@ +[[blockchains]] + chain_id = "1337" + docker_cmd_params = ["-b", "1", "--mixed-mining", "--slots-in-an-epoch", "1", "--base-fee", "1", "--gas-price", "1", "--balance", "100000"] + image = "ghcr.io/foundry-rs/foundry:stable" + port = "8545" + type = "anvil" + +[fake_server] + image = "chainlink-fakes:latest" + port = 9111 + +[[nodesets]] + name = "don" + nodes = 2 + override_mode = "each" + + [nodesets.db] + image = "postgres:15.0" + + [[nodesets.node_specs]] + + [nodesets.node_specs.node] + image = "chainlink-tmp:latest" + + [[nodesets.node_specs]] + + [nodesets.node_specs.node] + image = "chainlink-tmp:latest" diff --git a/devenv/env-vrfv2.toml b/devenv/env-vrfv2.toml new file mode 100644 index 00000000000..a547da3f629 --- /dev/null +++ b/devenv/env-vrfv2.toml @@ -0,0 +1,23 @@ +[[blockchains]] + chain_id = "1337" + docker_cmd_params = ["-b", "1", "--mixed-mining", "--slots-in-an-epoch", "1", "--base-fee", "1", "--gas-price", "1", "--balance", "100000"] + image = "ghcr.io/foundry-rs/foundry:stable" + port = "8545" + type = "anvil" + +[fake_server] + image = "chainlink-fakes:latest" + port = 9111 + +[[nodesets]] + name = "don" + nodes = 1 + override_mode = "each" + + [nodesets.db] + image = "postgres:15.0" + + [[nodesets.node_specs]] + + [nodesets.node_specs.node] + image = "chainlink-tmp:latest" diff --git a/devenv/environment.go b/devenv/environment.go index ff9ddabb2b1..c4647e7f7ee 100644 --- a/devenv/environment.go +++ b/devenv/environment.go @@ -20,6 +20,7 @@ import ( "github.com/smartcontractkit/chainlink/devenv/products/keepers" "github.com/smartcontractkit/chainlink/devenv/products/ocr2" "github.com/smartcontractkit/chainlink/devenv/products/vrf" + "github.com/smartcontractkit/chainlink/devenv/products/vrfv2" "github.com/smartcontractkit/chainlink/devenv/products/vrfv2plus" ) @@ -54,6 +55,8 @@ func newProduct(name string) (Product, error) { return vrf.NewConfigurator(), nil case "vrfv2_plus": return vrfv2plus.NewConfigurator(), nil + case "vrfv2": + return vrfv2.NewConfigurator(), nil default: return nil, fmt.Errorf("unknown product type: %s", name) diff --git a/devenv/go.mod b/devenv/go.mod index dad36d0d7ee..7507b500f4f 100644 --- a/devenv/go.mod +++ b/devenv/go.mod @@ -11,6 +11,7 @@ require ( github.com/docker/docker v28.5.1+incompatible github.com/ethereum/go-ethereum v1.17.1 github.com/go-resty/resty/v2 v2.17.1 + github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.11.1 @@ -19,6 +20,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 github.com/scylladb/go-reflectx v1.0.1 + github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.97 github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-common v0.10.1-0.20260317233127-178dd2eeaa87 @@ -157,7 +159,6 @@ require ( github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect @@ -283,7 +284,6 @@ require ( github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shirou/gopsutil/v4 v4.25.9 // indirect - github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/chainlink-common/keystore v1.0.2 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect diff --git a/devenv/products/evm.go b/devenv/products/evm.go index 04e56706400..d15c9cf512f 100644 --- a/devenv/products/evm.go +++ b/devenv/products/evm.go @@ -8,9 +8,13 @@ import ( "math/big" "os" "strings" + "testing" "time" + "github.com/onsi/gomega" pkgerrors "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -21,6 +25,7 @@ import ( "github.com/rs/zerolog" "github.com/smartcontractkit/chainlink-testing-framework/seth" + "github.com/smartcontractkit/chainlink/devenv/contracts" ) const ( @@ -409,3 +414,77 @@ func InitSeth(rpcURL string, privateKeys []string, chainID *uint64) (*seth.Clien return chainClient, err } + +// WaitUntilChainHead blocks until the chain head is at least anchorBlock + minBlocksAfterAnchor +// On Anvil (chainID 1337) it deploys a Counter and spams Increment txs so blocks advance quickly; +// on other chains it polls block number until the target is reached. +func WaitUntilChainHead( + ctx context.Context, + t *testing.T, + chainClient *seth.Client, + anchorBlock uint64, + minBlocksAfterAnchor int, + chainID uint64, + timeout time.Duration, +) { + t.Helper() + require.GreaterOrEqual(t, minBlocksAfterAnchor, 0, "minBlocksAfterAnchor must be non-negative") + + targetBlock := anchorBlock + uint64(minBlocksAfterAnchor) //nolint:gosec // minBlocksAfterAnchor validated non-negative above + if chainID != 1337 { + gomega.NewGomegaWithT(t).Eventually(func() bool { + blk, err := chainClient.Client.BlockNumber(ctx) + if err != nil { + return false + } + return blk >= targetBlock + }, timeout, time.Second).Should(gomega.BeTrue(), + "timed out waiting for chain to reach block %d", targetBlock) + return + } + + counter, err := contracts.DeployCounterContract(chainClient) + require.NoError(t, err, "failed to deploy counter contract for tx-spam block advancement") + err = counter.Reset() + require.NoError(t, err, "failed to reset counter contract for tx-spam") + + waitCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + done := make(chan struct{}) + var eg errgroup.Group + eg.Go(func() error { + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-waitCtx.Done(): + return fmt.Errorf("timeout waiting for chain to reach block %d", targetBlock) + case <-ticker.C: + blk, bErr := chainClient.Client.BlockNumber(waitCtx) + if bErr != nil { + continue + } + if blk >= targetBlock { + close(done) + return nil + } + } + } + }) + eg.Go(func() error { + for { + select { + case <-done: + return nil + case <-waitCtx.Done(): + return fmt.Errorf("timeout while generating txs waiting for block %d", targetBlock) + default: + if iErr := counter.Increment(); iErr != nil { + return iErr + } + } + } + }) + require.NoError(t, eg.Wait(), "failed while waiting for min chain head with tx-spam enabled on chainID=1337") +} diff --git a/devenv/products/vrfv2/basic.toml b/devenv/products/vrfv2/basic.toml new file mode 100644 index 00000000000..1f1357bb612 --- /dev/null +++ b/devenv/products/vrfv2/basic.toml @@ -0,0 +1,54 @@ +[[products]] +name = "vrfv2" +instances = 1 + +[[vrfv2]] +num_tx_keys = 0 +cl_nodes_funding_eth = 5.0 +cl_node_max_gas_price_gwei = 400 + +minimum_confirmations = 3 +max_gas_limit_coordinator = 2500000 +staleness_seconds = 86400 +gas_after_payment_calculation = 33825 +fallback_wei_per_unit_link = "60000000000000000" + +fulfillment_flat_fee_link_ppm_tier_1 = 500 +fulfillment_flat_fee_link_ppm_tier_2 = 500 +fulfillment_flat_fee_link_ppm_tier_3 = 500 +fulfillment_flat_fee_link_ppm_tier_4 = 500 +fulfillment_flat_fee_link_ppm_tier_5 = 500 +reqs_for_tier_2 = 0 +reqs_for_tier_3 = 0 +reqs_for_tier_4 = 0 +reqs_for_tier_5 = 0 + +vrf_job_forwarding_allowed = false +vrf_job_estimate_gas_multiplier = 1.1 +vrf_job_batch_fulfillment_enabled = true +vrf_job_batch_fulfillment_gas_multiplier = 1.15 +vrf_job_poll_period = "1s" +vrf_job_request_timeout = "24h" + +sub_funding_amount_link = 5.0 + +number_of_words = 3 +callback_gas_limit = 1000000 +randomness_request_count_per_request = 1 +randomness_request_count_per_request_deviation = 0 +random_words_fulfilled_event_timeout = "2m" + +wrapper_gas_overhead = 50000 +coordinator_gas_overhead = 52000 +wrapper_premium_percentage = 25 +wrapper_max_number_of_words = 10 +wrapper_consumer_funding_amount_link = 10 + +enable_bhs_job = false + +batch_callback_gas_limit = 500000 +batch_tx_gas_budget = 2900000 + +[vrfv2.gas_settings] +fee_cap_multiplier = 2 +tip_cap_multiplier = 2 diff --git a/devenv/products/vrfv2/bhs.toml b/devenv/products/vrfv2/bhs.toml new file mode 100644 index 00000000000..855ed8ad41c --- /dev/null +++ b/devenv/products/vrfv2/bhs.toml @@ -0,0 +1,58 @@ +[[products]] +name = "vrfv2" +instances = 1 + +[[vrfv2]] +enable_bhs_job = true +bhs_job_wait_blocks = 2 +bhs_job_lookback_blocks = 20 +bhs_job_poll_period = "1s" +bhs_job_run_timeout = "30s" + +num_tx_keys = 0 +cl_nodes_funding_eth = 5.0 +cl_node_max_gas_price_gwei = 400 + +minimum_confirmations = 3 +max_gas_limit_coordinator = 2500000 +staleness_seconds = 86400 +gas_after_payment_calculation = 33825 +fallback_wei_per_unit_link = "60000000000000000" + +fulfillment_flat_fee_link_ppm_tier_1 = 500 +fulfillment_flat_fee_link_ppm_tier_2 = 500 +fulfillment_flat_fee_link_ppm_tier_3 = 500 +fulfillment_flat_fee_link_ppm_tier_4 = 500 +fulfillment_flat_fee_link_ppm_tier_5 = 500 +reqs_for_tier_2 = 0 +reqs_for_tier_3 = 0 +reqs_for_tier_4 = 0 +reqs_for_tier_5 = 0 + +vrf_job_forwarding_allowed = false +vrf_job_estimate_gas_multiplier = 1.1 +vrf_job_batch_fulfillment_enabled = true +vrf_job_batch_fulfillment_gas_multiplier = 1.15 +vrf_job_poll_period = "1s" +vrf_job_request_timeout = "24h" + +sub_funding_amount_link = 5.0 + +number_of_words = 3 +callback_gas_limit = 1000000 +randomness_request_count_per_request = 1 +randomness_request_count_per_request_deviation = 0 +random_words_fulfilled_event_timeout = "2m" + +wrapper_gas_overhead = 50000 +coordinator_gas_overhead = 52000 +wrapper_premium_percentage = 25 +wrapper_max_number_of_words = 10 +wrapper_consumer_funding_amount_link = 10 + +batch_callback_gas_limit = 500000 +batch_tx_gas_budget = 2900000 + +[vrfv2.gas_settings] +fee_cap_multiplier = 2 +tip_cap_multiplier = 2 diff --git a/devenv/products/vrfv2/configuration.go b/devenv/products/vrfv2/configuration.go new file mode 100644 index 00000000000..836f7285b7f --- /dev/null +++ b/devenv/products/vrfv2/configuration.go @@ -0,0 +1,121 @@ +package vrfv2 + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink/devenv/products" +) + +// Configurator implements the devenv Product interface for classic VRF Coordinator V2. +type Configurator struct { + Config []*VRFv2 `toml:"vrfv2"` + + nodeEVMKeyAddr string + nodeEVMKeyEncJSON []byte + nodeEVMKeyPass string + + txKeyAddrs []string + txKeyEncJSONs [][]byte + + bhsKeyAddr string + bhsKeyEncJSON []byte +} + +// VRFv2 holds per-instance configuration for vrfv2 product. +type VRFv2 struct { + CLNodesFundingETH float64 `toml:"cl_nodes_funding_eth"` + CLNodeMaxGasPriceGWei int64 `toml:"cl_node_max_gas_price_gwei"` + GasSettings products.GasSettings `toml:"gas_settings"` + + NumTxKeys int `toml:"num_tx_keys"` + + MinimumConfirmations uint16 `toml:"minimum_confirmations"` + MaxGasLimitCoordinator uint32 `toml:"max_gas_limit_coordinator"` + StalenessSeconds uint32 `toml:"staleness_seconds"` + GasAfterPaymentCalculation uint32 `toml:"gas_after_payment_calculation"` + FallbackWeiPerUnitLink string `toml:"fallback_wei_per_unit_link"` + + FulfillmentFlatFeeLinkPPMTier1 uint32 `toml:"fulfillment_flat_fee_link_ppm_tier_1"` + FulfillmentFlatFeeLinkPPMTier2 uint32 `toml:"fulfillment_flat_fee_link_ppm_tier_2"` + FulfillmentFlatFeeLinkPPMTier3 uint32 `toml:"fulfillment_flat_fee_link_ppm_tier_3"` + FulfillmentFlatFeeLinkPPMTier4 uint32 `toml:"fulfillment_flat_fee_link_ppm_tier_4"` + FulfillmentFlatFeeLinkPPMTier5 uint32 `toml:"fulfillment_flat_fee_link_ppm_tier_5"` + ReqsForTier2 int64 `toml:"reqs_for_tier_2"` + ReqsForTier3 int64 `toml:"reqs_for_tier_3"` + ReqsForTier4 int64 `toml:"reqs_for_tier_4"` + ReqsForTier5 int64 `toml:"reqs_for_tier_5"` + + VRFJobForwardingAllowed bool `toml:"vrf_job_forwarding_allowed"` + VRFJobEstimateGasMultiplier float64 `toml:"vrf_job_estimate_gas_multiplier"` + VRFJobBatchFulfillmentEnabled bool `toml:"vrf_job_batch_fulfillment_enabled"` + VRFJobBatchFulfillmentGasMultiplier float64 `toml:"vrf_job_batch_fulfillment_gas_multiplier"` + VRFJobPollPeriod string `toml:"vrf_job_poll_period"` + VRFJobRequestTimeout string `toml:"vrf_job_request_timeout"` + VRFJobSimulationBlock string `toml:"vrf_job_simulation_block"` + + SubFundingAmountLink float64 `toml:"sub_funding_amount_link"` + + WrapperGasOverhead uint32 `toml:"wrapper_gas_overhead"` + CoordinatorGasOverhead uint32 `toml:"coordinator_gas_overhead"` + WrapperPremiumPercentage uint8 `toml:"wrapper_premium_percentage"` + WrapperMaxNumberOfWords uint8 `toml:"wrapper_max_number_of_words"` + WrapperConsumerFundingAmountLink float64 `toml:"wrapper_consumer_funding_amount_link"` + + NumberOfWords uint32 `toml:"number_of_words"` + CallbackGasLimit uint32 `toml:"callback_gas_limit"` + RandomnessRequestCountPerRequest uint16 `toml:"randomness_request_count_per_request"` + RandomnessRequestCountPerRequestDeviation uint16 `toml:"randomness_request_count_per_request_deviation"` + RandomWordsFulfilledEventTimeout string `toml:"random_words_fulfilled_event_timeout"` + + BatchCallbackGasLimit uint32 `toml:"batch_callback_gas_limit"` + BatchTxGasBudget uint32 `toml:"batch_tx_gas_budget"` + + EnableBHSJob bool `toml:"enable_bhs_job"` + BHSJobWaitBlocks int `toml:"bhs_job_wait_blocks"` + BHSJobLookbackBlocks int `toml:"bhs_job_lookback_blocks"` + BHSJobPollPeriod string `toml:"bhs_job_poll_period"` + BHSJobRunTimeout string `toml:"bhs_job_run_timeout"` + + DeployedContracts DeployedContracts `toml:"deployed_contracts"` + VRFKeyData KeyOutput `toml:"vrf_key_data"` +} + +// DeployedContracts holds deployed contract addresses for VRF v2 smoke. +type DeployedContracts struct { + LinkToken string `toml:"link_token"` + MockFeed string `toml:"mock_feed"` + BHS string `toml:"bhs"` + Coordinator string `toml:"coordinator"` + BatchCoordinator string `toml:"batch_coordinator"` +} + +// KeyOutput is persisted to env-out for tests. +type KeyOutput struct { + PubKeyCompressed string `toml:"pub_key_compressed"` + PubKeyUncompressed string `toml:"pub_key_uncompressed"` + KeyHash string `toml:"key_hash"` + VRFJobID string `toml:"vrf_job_id"` + TxKeyAddresses []string `toml:"tx_key_addresses"` + BHSJobID string `toml:"bhs_job_id"` +} + +// NewConfigurator returns a new vrfv2 configurator. +func NewConfigurator() *Configurator { + return &Configurator{} +} + +func (m *Configurator) Load() error { + cfg, err := products.Load[Configurator]() + if err != nil { + return fmt.Errorf("failed to load vrfv2 product config: %w", err) + } + m.Config = cfg.Config + return nil +} + +func (m *Configurator) Store(path string, instanceIdx int) error { + if err := products.Store(".", &Configurator{Config: []*VRFv2{m.Config[instanceIdx]}}); err != nil { + return fmt.Errorf("failed to store vrfv2 product config: %w", err) + } + return nil +} diff --git a/devenv/products/vrfv2/core.go b/devenv/products/vrfv2/core.go new file mode 100644 index 00000000000..4d3bc0c7d4f --- /dev/null +++ b/devenv/products/vrfv2/core.go @@ -0,0 +1,454 @@ +package vrfv2 + +import ( + "bytes" + "context" + "errors" + "fmt" + "math/big" + "os" + "strconv" + "text/template" + "time" + + "github.com/google/uuid" + "github.com/pelletier/go-toml/v2" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/vrf_coordinator_v2" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake" + nodeset "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" + + "github.com/smartcontractkit/chainlink/devenv/contracts" + "github.com/smartcontractkit/chainlink/devenv/products" +) + +var L = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.DebugLevel).With().Fields(map[string]any{"component": "vrfv2"}).Logger() + +const txKeyPassword = "txkey-password" + +func (m *Configurator) GenerateNodesConfig( + ctx context.Context, + fs *fake.Input, + bc []*blockchain.Input, + ns []*nodeset.Input, +) (string, error) { + cfg := m.Config[0] + + L.Info().Msg("Pre-generating primary EVM key for VRF node") + encJSON, addr, err := clclient.NewETHKey(txKeyPassword) + if err != nil { + return "", fmt.Errorf("failed to generate primary ETH key: %w", err) + } + m.nodeEVMKeyAddr = addr.Hex() + m.nodeEVMKeyEncJSON = encJSON + m.nodeEVMKeyPass = txKeyPassword + + m.txKeyAddrs = make([]string, 0, cfg.NumTxKeys) + m.txKeyEncJSONs = make([][]byte, 0, cfg.NumTxKeys) + for i := 0; i < cfg.NumTxKeys; i++ { + enc, a, kErr := clclient.NewETHKey(txKeyPassword) + if kErr != nil { + return "", fmt.Errorf("failed to generate extra TX key %d: %w", i, kErr) + } + m.txKeyAddrs = append(m.txKeyAddrs, a.Hex()) + m.txKeyEncJSONs = append(m.txKeyEncJSONs, enc) + } + + if cfg.EnableBHSJob { + enc, a, kErr := clclient.NewETHKey(txKeyPassword) + if kErr != nil { + return "", fmt.Errorf("failed to generate BHS TX key: %w", kErr) + } + m.bhsKeyAddr = a.Hex() + m.bhsKeyEncJSON = enc + } + + baseConfig := `[Feature] +FeedsManager = true +LogPoller = true +UICSAKeys = true + +[Log] +Level = 'debug' +JSONConsole = true + +[Log.File] +MaxSize = '0b' + +[WebServer] +AllowOrigins = '*' +HTTPPort = 6688 +SecureCookies = false +HTTPWriteTimeout = '3m' +SessionTimeout = '999h0m0s' + +[WebServer.RateLimit] +Authenticated = 2000 +Unauthenticated = 1000 + +[WebServer.TLS] +HTTPSPort = 0 + +[OCR2] +Enabled = true + +[P2P] +[P2P.V2] +Enabled = true +ListenAddresses = ['0.0.0.0:6690'] +AnnounceAddresses = ['0.0.0.0:6690'] +` + + netConfigTemplate := ` +[[EVM]] +AutoCreateKey = true +MinContractPayment = 0 +BlockBackfillDepth = 100 +MinIncomingConfirmations = 1 + +ChainID = '{{.ChainID}}' + +[EVM.GasEstimator] +LimitDefault = {{.TxGasLimitDefault}} +LimitMax = {{.TxGasLimitDefault}} + +[[EVM.Nodes]] +Name = 'default' +WsUrl = '{{.WsURL}}' +HttpUrl = '{{.HTTPURL}}' +{{range .Keys}} +[[EVM.KeySpecific]] +Key = '{{.}}' +GasEstimator.PriceMax = '{{$.MaxGasPriceGWei}} gwei' +{{end}}` + + tmpl, err := template.New("vrfv2-net-config").Parse(netConfigTemplate) + if err != nil { + return "", fmt.Errorf("failed to parse VRF net config template: %w", err) + } + + allKeys := append([]string{m.nodeEVMKeyAddr}, m.txKeyAddrs...) + if m.bhsKeyAddr != "" { + allKeys = append(allKeys, m.bhsKeyAddr) + } + + type data struct { + ChainID string + WsURL string + HTTPURL string + Keys []string + MaxGasPriceGWei int64 + TxGasLimitDefault uint32 + } + + txGasLimitDefault := uint32(3_500_000) + d := data{ + ChainID: bc[0].Out.ChainID, + WsURL: bc[0].Out.Nodes[0].InternalWSUrl, + HTTPURL: bc[0].Out.Nodes[0].InternalHTTPUrl, + Keys: allKeys, + MaxGasPriceGWei: cfg.CLNodeMaxGasPriceGWei, + TxGasLimitDefault: txGasLimitDefault, + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, d); err != nil { + return "", fmt.Errorf("failed to execute VRF net config template: %w", err) + } + + return baseConfig + buf.String(), nil +} + +func (m *Configurator) GenerateNodesSecrets( + _ context.Context, + _ *fake.Input, + bc []*blockchain.Input, + _ []*nodeset.Input, +) (string, error) { + chainID, err := strconv.ParseInt(bc[0].Out.ChainID, 10, 64) + if err != nil { + return "", fmt.Errorf("failed to parse chainID: %w", err) + } + + type evmKey struct { + JSON string `toml:"JSON"` + Password string `toml:"Password"` + ID int64 `toml:"ID"` + } + type evmSecrets struct { + Keys []evmKey `toml:"Keys"` + } + type secretsDoc struct { + EVM evmSecrets `toml:"EVM"` + } + + keys := []evmKey{ + {JSON: string(m.nodeEVMKeyEncJSON), Password: m.nodeEVMKeyPass, ID: chainID}, + } + for _, enc := range m.txKeyEncJSONs { + keys = append(keys, evmKey{JSON: string(enc), Password: txKeyPassword, ID: chainID}) + } + if len(m.bhsKeyEncJSON) > 0 { + keys = append(keys, evmKey{JSON: string(m.bhsKeyEncJSON), Password: txKeyPassword, ID: chainID}) + } + + doc := secretsDoc{EVM: evmSecrets{Keys: keys}} + out, err := toml.Marshal(doc) + if err != nil { + return "", fmt.Errorf("failed to marshal node secrets: %w", err) + } + L.Info().Int("num_keys", len(keys)).Msg("EVM keys marshalled into secrets") + return string(out), nil +} + +func (m *Configurator) ConfigureJobsAndContracts( + ctx context.Context, + instanceIdx int, + _ *fake.Input, + bc []*blockchain.Input, + ns []*nodeset.Input, +) error { + cfg := m.Config[instanceIdx] + + if err := validateTopology(cfg, ns[0]); err != nil { + return err + } + + cl, err := clclient.New(ns[0].Out.CLNodes) + if err != nil { + return fmt.Errorf("failed to connect to CL nodes: %w", err) + } + + pkey := products.NetworkPrivateKey() + if pkey == "" { + return errors.New("PRIVATE_KEY environment variable not set") + } + + bcNode := bc[0].Out.Nodes[0] + c, _, _, err := products.ETHClient( + ctx, + bcNode.ExternalWSUrl, + cfg.GasSettings.FeeCapMultiplier, + cfg.GasSettings.TipCapMultiplier, + ) + if err != nil { + return fmt.Errorf("could not create basic eth client: %w", err) + } + + addrsToFund := append([]string{m.nodeEVMKeyAddr}, m.txKeyAddrs...) + if m.bhsKeyAddr != "" { + addrsToFund = append(addrsToFund, m.bhsKeyAddr) + } + for _, addr := range addrsToFund { + L.Info().Str("addr", addr).Float64("eth", cfg.CLNodesFundingETH).Msg("Funding EVM address") + if fErr := products.FundAddressEIP1559(ctx, c, pkey, addr, cfg.CLNodesFundingETH); fErr != nil { + return fmt.Errorf("failed to fund address %s: %w", addr, fErr) + } + } + + chainID, err := strconv.ParseUint(bc[0].Out.ChainID, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse chainID: %w", err) + } + + chainClient, err := products.InitSeth(bcNode.ExternalWSUrl, []string{pkey}, &chainID) + if err != nil { + return fmt.Errorf("failed to init seth client: %w", err) + } + + L.Info().Msg("Deploying BlockhashStore") + bhs, err := contracts.DeployBlockhashStore(chainClient) + if err != nil { + return fmt.Errorf("failed to deploy BlockhashStore: %w", err) + } + cfg.DeployedContracts.BHS = bhs.Address() + + L.Info().Msg("Deploying LINK token") + linkToken, err := contracts.DeployLinkTokenContract(L, chainClient) + if err != nil { + return fmt.Errorf("failed to deploy LINK token: %w", err) + } + cfg.DeployedContracts.LinkToken = linkToken.Address() + + L.Info().Msg("Deploying Mock LINK/ETH feed") + mockFeed, err := contracts.DeployMockLINKETHFeed(chainClient, big.NewInt(1e18)) + if err != nil { + return fmt.Errorf("failed to deploy MockLINKETHFeed: %w", err) + } + cfg.DeployedContracts.MockFeed = mockFeed.Address() + + L.Info().Msg("Deploying VRFCoordinatorV2") + coord, err := contracts.DeployVRFCoordinatorV2(chainClient, linkToken.Address(), bhs.Address(), mockFeed.Address()) + if err != nil { + return fmt.Errorf("failed to deploy VRFCoordinatorV2: %w", err) + } + cfg.DeployedContracts.Coordinator = coord.Address() + + L.Info().Msg("Deploying BatchVRFCoordinatorV2") + batchCoord, err := contracts.DeployBatchVRFCoordinatorV2(chainClient, coord.Address()) + if err != nil { + return fmt.Errorf("failed to deploy BatchVRFCoordinatorV2: %w", err) + } + cfg.DeployedContracts.BatchCoordinator = batchCoord.Address() + + fallbackWei, err := contracts.FallbackWeiBigInt(cfg.FallbackWeiPerUnitLink) + if err != nil { + return fmt.Errorf("invalid fallback_wei_per_unit_link: %w", err) + } + + feeConfig := vrf_coordinator_v2.VRFCoordinatorV2FeeConfig{ + FulfillmentFlatFeeLinkPPMTier1: cfg.FulfillmentFlatFeeLinkPPMTier1, + FulfillmentFlatFeeLinkPPMTier2: cfg.FulfillmentFlatFeeLinkPPMTier2, + FulfillmentFlatFeeLinkPPMTier3: cfg.FulfillmentFlatFeeLinkPPMTier3, + FulfillmentFlatFeeLinkPPMTier4: cfg.FulfillmentFlatFeeLinkPPMTier4, + FulfillmentFlatFeeLinkPPMTier5: cfg.FulfillmentFlatFeeLinkPPMTier5, + ReqsForTier2: big.NewInt(cfg.ReqsForTier2), + ReqsForTier3: big.NewInt(cfg.ReqsForTier3), + ReqsForTier4: big.NewInt(cfg.ReqsForTier4), + ReqsForTier5: big.NewInt(cfg.ReqsForTier5), + } + + L.Info().Msg("Setting VRFCoordinatorV2 config") + if err := coord.SetConfig( + cfg.MinimumConfirmations, + cfg.MaxGasLimitCoordinator, + cfg.StalenessSeconds, + cfg.GasAfterPaymentCalculation, + fallbackWei, + feeConfig, + ); err != nil { + return fmt.Errorf("coordinator SetConfig failed: %w", err) + } + + L.Info().Msg("Creating VRF key on CL node") + vrfKey, err := cl[0].MustCreateVRFKey() + if err != nil { + return fmt.Errorf("failed to create VRF key: %w", err) + } + + provingKey, err := contracts.EncodeOnChainVRFProvingKey(vrfKey.Data.Attributes.Uncompressed) + if err != nil { + return fmt.Errorf("failed to encode VRF proving key: %w", err) + } + + rootAddr := chainClient.MustGetRootKeyAddress() + L.Info().Str("oracle", rootAddr.Hex()).Msg("Registering VRF proving key on coordinator") + if err := coord.RegisterProvingKey(rootAddr.Hex(), provingKey); err != nil { + return fmt.Errorf("failed to register proving key: %w", err) + } + + keyHash, err := coord.HashOfKey(ctx, provingKey) + if err != nil { + return fmt.Errorf("failed to get key hash: %w", err) + } + cfg.VRFKeyData.PubKeyCompressed = vrfKey.Data.ID + cfg.VRFKeyData.PubKeyUncompressed = vrfKey.Data.Attributes.Uncompressed + cfg.VRFKeyData.KeyHash = fmt.Sprintf("0x%x", keyHash) + + pollPeriod, err := time.ParseDuration(cfg.VRFJobPollPeriod) + if err != nil { + pollPeriod = time.Second + } + requestTimeout, err := time.ParseDuration(cfg.VRFJobRequestTimeout) + if err != nil { + requestTimeout = 24 * time.Hour + } + + fromAddresses := append([]string{m.nodeEVMKeyAddr}, m.txKeyAddrs...) + + pipelineSpec := &TxPipelineSpec{ + Address: coord.Address(), + EstimateGasMultiplier: cfg.VRFJobEstimateGasMultiplier, + FromAddress: fromAddresses[0], + } + if cfg.VRFJobSimulationBlock != "" { + s := cfg.VRFJobSimulationBlock + pipelineSpec.SimulationBlock = &s + } + observationSource, err := pipelineSpec.String() + if err != nil { + return fmt.Errorf("failed to build VRF pipeline spec: %w", err) + } + + batchGasMult := cfg.VRFJobBatchFulfillmentGasMultiplier + if batchGasMult == 0 { + batchGasMult = 1.1 + } + + jobSpec := &JobSpec{ + Name: "vrf-v2", + CoordinatorAddress: coord.Address(), + BatchCoordinatorAddress: batchCoord.Address(), + PublicKey: vrfKey.Data.ID, + ExternalJobID: uuid.New().String(), + ObservationSource: observationSource, + MinIncomingConfirmations: int(cfg.MinimumConfirmations), + FromAddresses: fromAddresses, + EVMChainID: bc[0].Out.ChainID, + ForwardingAllowed: cfg.VRFJobForwardingAllowed, + BatchFulfillmentEnabled: cfg.VRFJobBatchFulfillmentEnabled, + BatchFulfillmentGasMultiplier: batchGasMult, + BackOffInitialDelay: 15 * time.Second, + BackOffMaxDelay: 5 * time.Minute, + PollPeriod: pollPeriod, + RequestTimeout: requestTimeout, + } + + L.Info().Msg("Creating VRF job on CL node") + job, err := cl[0].MustCreateJob(jobSpec) + if err != nil { + return fmt.Errorf("failed to create VRF job: %w", err) + } + cfg.VRFKeyData.VRFJobID = job.Data.ID + + if cfg.EnableBHSJob { + coordinatorAddr := coord.Address() + bhsJob, bhsErr := cl[1].MustCreateJob(&BlockhashStoreJobSpec{ + Name: "bhs-vrf-v2", + ExternalJobID: uuid.New().String(), + CoordinatorV2Address: coordinatorAddr, + CoordinatorV2PlusAddress: coordinatorAddr, + BlockhashStoreAddress: bhs.Address(), + FromAddresses: []string{m.bhsKeyAddr}, + EVMChainID: bc[0].Out.ChainID, + WaitBlocks: cfg.BHSJobWaitBlocks, + LookbackBlocks: cfg.BHSJobLookbackBlocks, + PollPeriod: cfg.BHSJobPollPeriod, + RunTimeout: cfg.BHSJobRunTimeout, + }) + if bhsErr != nil { + return fmt.Errorf("failed to create BHS job: %w", bhsErr) + } + cfg.VRFKeyData.BHSJobID = bhsJob.Data.ID + L.Info().Str("bhs_job_id", cfg.VRFKeyData.BHSJobID).Msg("BHS job created") + } + + cfg.VRFKeyData.TxKeyAddresses = append([]string{m.nodeEVMKeyAddr}, m.txKeyAddrs...) + + L.Info(). + Str("Coordinator", cfg.DeployedContracts.Coordinator). + Str("BatchCoordinator", cfg.DeployedContracts.BatchCoordinator). + Str("KeyHash", cfg.VRFKeyData.KeyHash). + Str("VRFJobID", cfg.VRFKeyData.VRFJobID). + Strs("TxKeyAddresses", cfg.VRFKeyData.TxKeyAddresses). + Msg("VRFv2 setup complete") + + return nil +} + +func validateTopology(cfg *VRFv2, ns *nodeset.Input) error { + got := len(ns.NodeSpecs) + want := 1 + if cfg.EnableBHSJob { + want++ + } + if got != want { + return fmt.Errorf("topology mismatch: nodeset has %d node(s), want %d (enable_bhs_job=%v)", + got, want, cfg.EnableBHSJob) + } + return nil +} diff --git a/devenv/products/vrfv2/job_spec.go b/devenv/products/vrfv2/job_spec.go new file mode 100644 index 00000000000..c7919dd2ee9 --- /dev/null +++ b/devenv/products/vrfv2/job_spec.go @@ -0,0 +1,118 @@ +package vrfv2 + +import ( + "bytes" + "fmt" + "text/template" + "time" +) + +// TxPipelineSpec is the classic VRF Coordinator V2 observation source (vrf_pipeline_v2). +type TxPipelineSpec struct { + Address string + EstimateGasMultiplier float64 + FromAddress string + SimulationBlock *string +} + +func (d *TxPipelineSpec) Type() string { return "vrf_pipeline_v2" } + +func (d *TxPipelineSpec) String() (string, error) { + optionalSimBlock := "" + if d.SimulationBlock != nil { + sb := *d.SimulationBlock + if sb != "latest" && sb != "pending" { + return "", fmt.Errorf("invalid SimulationBlock value: %s", sb) + } + optionalSimBlock = fmt.Sprintf("block=\"%s\"", sb) + } + sourceTemplate := ` +decode_log [type=ethabidecodelog + abi="RandomWordsRequested(bytes32 indexed keyHash,uint256 requestId,uint256 preSeed,uint64 indexed subId,uint16 minimumRequestConfirmations,uint32 callbackGasLimit,uint32 numWords,address indexed sender)" + data="$(jobRun.logData)" + topics="$(jobRun.logTopics)"] +vrf [type=vrfv2 + publicKey="$(jobSpec.publicKey)" + requestBlockHash="$(jobRun.logBlockHash)" + requestBlockNumber="$(jobRun.logBlockNumber)" + topics="$(jobRun.logTopics)"] +estimate_gas [type=estimategaslimit + to="{{ .Address }}" + multiplier="{{ .EstimateGasMultiplier }}" + data="$(vrf.output)" + %s] +simulate [type=ethcall + from="{{ .FromAddress }}" + to="{{ .Address }}" + gas="$(estimate_gas)" + gasPrice="$(jobSpec.maxGasPrice)" + extractRevertReason=true + contract="{{ .Address }}" + data="$(vrf.output)" + %s] +decode_log->vrf->estimate_gas->simulate` + + sourceString := fmt.Sprintf(sourceTemplate, optionalSimBlock, optionalSimBlock) + return marshallTemplate(d, "VRFv2 pipeline template", sourceString) +} + +// JobSpec is the full VRF v2 job TOML (type vrf). +type JobSpec struct { + Name string `toml:"name"` + CoordinatorAddress string `toml:"coordinatorAddress"` + BatchCoordinatorAddress string `toml:"batchCoordinatorAddress"` + PublicKey string `toml:"publicKey"` + ExternalJobID string `toml:"externalJobID"` + ObservationSource string `toml:"observationSource"` + MinIncomingConfirmations int `toml:"minIncomingConfirmations"` + FromAddresses []string `toml:"fromAddresses"` + EVMChainID string `toml:"evmChainID"` + ForwardingAllowed bool `toml:"forwardingAllowed"` + BatchFulfillmentEnabled bool `toml:"batchFulfillmentEnabled"` + BatchFulfillmentGasMultiplier float64 `toml:"batchFulfillmentGasMultiplier"` + BackOffInitialDelay time.Duration `toml:"backOffInitialDelay"` + BackOffMaxDelay time.Duration `toml:"backOffMaxDelay"` + PollPeriod time.Duration `toml:"pollPeriod"` + RequestTimeout time.Duration `toml:"requestTimeout"` +} + +func (v *JobSpec) Type() string { return "vrf" } + +func (v *JobSpec) String() (string, error) { + vrfTemplateString := ` +type = "vrf" +schemaVersion = 1 +name = "{{.Name}}" +forwardingAllowed = {{.ForwardingAllowed}} +coordinatorAddress = "{{.CoordinatorAddress}}" +{{ if .BatchFulfillmentEnabled }}batchCoordinatorAddress = "{{.BatchCoordinatorAddress}}"{{ else }}{{ end }} +fromAddresses = [{{range .FromAddresses}}"{{.}}",{{end}}] +evmChainID = "{{.EVMChainID}}" +minIncomingConfirmations = {{.MinIncomingConfirmations}} +publicKey = "{{.PublicKey}}" +externalJobID = "{{.ExternalJobID}}" +batchFulfillmentEnabled = {{.BatchFulfillmentEnabled}} +batchFulfillmentGasMultiplier = {{.BatchFulfillmentGasMultiplier}} +backoffInitialDelay = "{{.BackOffInitialDelay}}" +backoffMaxDelay = "{{.BackOffMaxDelay}}" +pollPeriod = "{{.PollPeriod}}" +requestTimeout = "{{.RequestTimeout}}" +customRevertsPipelineEnabled = true +observationSource = """ +{{.ObservationSource}} +""" +` + return marshallTemplate(v, "VRFv2 Job", vrfTemplateString) +} + +func marshallTemplate(jobSpec any, name, templateString string) (string, error) { + var buf bytes.Buffer + tmpl, err := template.New(name).Parse(templateString) + if err != nil { + return "", err + } + if err := tmpl.Execute(&buf, jobSpec); err != nil { + return "", err + } + return buf.String(), nil +} diff --git a/devenv/products/vrfv2/job_spec_bhs.go b/devenv/products/vrfv2/job_spec_bhs.go new file mode 100644 index 00000000000..a925f2f8f29 --- /dev/null +++ b/devenv/products/vrfv2/job_spec_bhs.go @@ -0,0 +1,36 @@ +package vrfv2 + +// BlockhashStoreJobSpec defines the BHS job for VRF v2 (coordinator address used for both V2 and V2Plus fields). +type BlockhashStoreJobSpec struct { + Name string + ExternalJobID string + CoordinatorV2Address string + CoordinatorV2PlusAddress string + BlockhashStoreAddress string + FromAddresses []string + EVMChainID string + WaitBlocks int + LookbackBlocks int + PollPeriod string + RunTimeout string +} + +func (b *BlockhashStoreJobSpec) Type() string { return "blockhashstore" } + +func (b *BlockhashStoreJobSpec) String() (string, error) { + tmpl := `type = "blockhashstore" +schemaVersion = 1 +name = "{{.Name}}" +externalJobID = "{{.ExternalJobID}}" +evmChainID = "{{.EVMChainID}}" +coordinatorV2Address = "{{.CoordinatorV2Address}}" +coordinatorV2PlusAddress = "{{.CoordinatorV2PlusAddress}}" +blockhashStoreAddress = "{{.BlockhashStoreAddress}}" +waitBlocks = {{.WaitBlocks}} +lookbackBlocks = {{.LookbackBlocks}} +pollPeriod = "{{.PollPeriod}}" +runTimeout = "{{.RunTimeout}}" +fromAddresses = [{{range .FromAddresses}}"{{.}}",{{end}}] +` + return marshallTemplate(b, "BlockhashStore Job", tmpl) +} diff --git a/devenv/products/vrfv2/two_keys.toml b/devenv/products/vrfv2/two_keys.toml new file mode 100644 index 00000000000..54b5cf299ab --- /dev/null +++ b/devenv/products/vrfv2/two_keys.toml @@ -0,0 +1,54 @@ +[[products]] +name = "vrfv2" +instances = 1 + +[[vrfv2]] +num_tx_keys = 2 +cl_nodes_funding_eth = 5.0 +cl_node_max_gas_price_gwei = 400 + +minimum_confirmations = 3 +max_gas_limit_coordinator = 2500000 +staleness_seconds = 86400 +gas_after_payment_calculation = 33825 +fallback_wei_per_unit_link = "60000000000000000" + +fulfillment_flat_fee_link_ppm_tier_1 = 500 +fulfillment_flat_fee_link_ppm_tier_2 = 500 +fulfillment_flat_fee_link_ppm_tier_3 = 500 +fulfillment_flat_fee_link_ppm_tier_4 = 500 +fulfillment_flat_fee_link_ppm_tier_5 = 500 +reqs_for_tier_2 = 0 +reqs_for_tier_3 = 0 +reqs_for_tier_4 = 0 +reqs_for_tier_5 = 0 + +vrf_job_forwarding_allowed = false +vrf_job_estimate_gas_multiplier = 1.1 +vrf_job_batch_fulfillment_enabled = true +vrf_job_batch_fulfillment_gas_multiplier = 1.15 +vrf_job_poll_period = "1s" +vrf_job_request_timeout = "24h" + +sub_funding_amount_link = 5.0 + +number_of_words = 3 +callback_gas_limit = 1000000 +randomness_request_count_per_request = 1 +randomness_request_count_per_request_deviation = 0 +random_words_fulfilled_event_timeout = "2m" + +wrapper_gas_overhead = 50000 +coordinator_gas_overhead = 52000 +wrapper_premium_percentage = 25 +wrapper_max_number_of_words = 10 +wrapper_consumer_funding_amount_link = 10 + +enable_bhs_job = false + +batch_callback_gas_limit = 500000 +batch_tx_gas_budget = 2900000 + +[vrfv2.gas_settings] +fee_cap_multiplier = 2 +tip_cap_multiplier = 2 diff --git a/devenv/tests/logpoller/config.go b/devenv/tests/logpoller/config.go new file mode 100644 index 00000000000..9c8ef532eeb --- /dev/null +++ b/devenv/tests/logpoller/config.go @@ -0,0 +1,146 @@ +package logpoller + +import ( + "errors" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" +) + +type GeneratorType = string + +const ( + GeneratorType_WASP = "wasp" //nolint: revive //we feel like using underscores + GeneratorType_Looped = "looped" //nolint: revive //we feel like using underscores +) + +type Config struct { + General General + ChaosConfig *ChaosConfig + Wasp *WaspConfig + LoopedConfig *LoopedConfig +} + +func (c *Config) Validate() error { + err := c.General.Validate() + if err != nil { + return fmt.Errorf("General config validation failed: %w", err) + } + + switch c.General.Generator { + case GeneratorType_WASP: + if c.Wasp == nil { + return errors.New("wasp config is nil") + } + err = c.Wasp.Validate() + if err != nil { + return fmt.Errorf("wasp config validation failed: %w", err) + } + case GeneratorType_Looped: + if c.LoopedConfig == nil { + return errors.New("looped config is nil") + } + err = c.LoopedConfig.Validate() + if err != nil { + return fmt.Errorf("looped config validation failed: %w", err) + } + default: + return fmt.Errorf("unknown generator type: %s", c.General.Generator) + } + + if c.ChaosConfig != nil { + if err := c.ChaosConfig.Validate(); err != nil { + return fmt.Errorf("chaos config validation failed: %w", err) + } + } + + return nil +} + +type LoopedConfig struct { + ExecutionCount int + MinEmitWaitTimeMs int + MaxEmitWaitTimeMs int +} + +func (l *LoopedConfig) Validate() error { + if l.ExecutionCount == 0 { + return errors.New("execution_count must be set and > 0") + } + + if l.MinEmitWaitTimeMs == 0 { + return errors.New("min_emit_wait_time_ms must be set and > 0") + } + + if l.MaxEmitWaitTimeMs == 0 { + return errors.New("max_emit_wait_time_ms must be set and > 0") + } + + return nil +} + +type General struct { + Generator string + EventsToEmit []abi.Event + Contracts int + EventsPerTx int + FundingAmountEth float64 +} + +func (g *General) Validate() error { + if g.Generator == "" { + return errors.New("generator is empty") + } + + if g.Contracts == 0 { + return errors.New("contracts is 0, but must be > 0") + } + + if g.EventsPerTx == 0 { + return errors.New("events_per_tx is 0, but must be > 0") + } + + return nil +} + +type ChaosConfig struct { + ExperimentCount int + TargetComponent string +} + +func (c *ChaosConfig) Validate() error { + if c.ExperimentCount == 0 { + return errors.New("experiment_count must be > 0") + } + + return nil +} + +type WaspConfig struct { + RPS int64 `toml:"rps"` + LPS int64 `toml:"lps"` + RateLimitUnitDuration time.Duration `toml:"rate_limit_unit_duration"` + Duration time.Duration `toml:"duration"` + CallTimeout time.Duration `toml:"call_timeout"` +} + +func (w *WaspConfig) Validate() error { + if w.RPS == 0 && w.LPS == 0 { + return errors.New("either RPS or LPS needs to be a positive integer") + } + if w.RPS != 0 && w.LPS != 0 { + return errors.New("only one of RPS or LPS can be set") + } + if w.Duration == 0 { + return errors.New("duration must be set and > 0") + } + if w.CallTimeout == 0 { + return errors.New("call_timeout must be set and > 0") + } + if w.RateLimitUnitDuration == 0 { + return errors.New("rate_limit_unit_duration must be set and > 0") + } + + return nil +} diff --git a/devenv/tests/logpoller/logpoller_test.go b/devenv/tests/logpoller/logpoller_test.go index 6238299b6df..4738d6907a1 100644 --- a/devenv/tests/logpoller/logpoller_test.go +++ b/devenv/tests/logpoller/logpoller_test.go @@ -3,7 +3,6 @@ package logpoller import ( "context" "crypto/ecdsa" - "errors" "fmt" "math" "math/big" @@ -28,143 +27,6 @@ import ( "github.com/smartcontractkit/chainlink/devenv/products/automation" ) -type GeneratorType = string - -const ( - GeneratorType_WASP = "wasp" //nolint: revive //we feel like using underscores - GeneratorType_Looped = "looped" //nolint: revive //we feel like using underscores -) - -type Config struct { - General General - ChaosConfig *ChaosConfig - Wasp *WaspConfig - LoopedConfig *LoopedConfig -} - -func (c *Config) Validate() error { - err := c.General.Validate() - if err != nil { - return fmt.Errorf("General config validation failed: %w", err) - } - - switch c.General.Generator { - case GeneratorType_WASP: - if c.Wasp == nil { - return errors.New("wasp config is nil") - } - err = c.Wasp.Validate() - if err != nil { - return fmt.Errorf("wasp config validation failed: %w", err) - } - case GeneratorType_Looped: - if c.LoopedConfig == nil { - return errors.New("looped config is nil") - } - err = c.LoopedConfig.Validate() - if err != nil { - return fmt.Errorf("looped config validation failed: %w", err) - } - default: - return fmt.Errorf("unknown generator type: %s", c.General.Generator) - } - - if c.ChaosConfig != nil { - if err := c.ChaosConfig.Validate(); err != nil { - return fmt.Errorf("chaos config validation failed: %w", err) - } - } - - return nil -} - -type LoopedConfig struct { - ExecutionCount int - MinEmitWaitTimeMs int - MaxEmitWaitTimeMs int -} - -func (l *LoopedConfig) Validate() error { - if l.ExecutionCount == 0 { - return errors.New("execution_count must be set and > 0") - } - - if l.MinEmitWaitTimeMs == 0 { - return errors.New("min_emit_wait_time_ms must be set and > 0") - } - - if l.MaxEmitWaitTimeMs == 0 { - return errors.New("max_emit_wait_time_ms must be set and > 0") - } - - return nil -} - -type General struct { - Generator string - EventsToEmit []abi.Event - Contracts int - EventsPerTx int - FundingAmountEth float64 -} - -func (g *General) Validate() error { - if g.Generator == "" { - return errors.New("generator is empty") - } - - if g.Contracts == 0 { - return errors.New("contracts is 0, but must be > 0") - } - - if g.EventsPerTx == 0 { - return errors.New("events_per_tx is 0, but must be > 0") - } - - return nil -} - -type ChaosConfig struct { - ExperimentCount int - TargetComponent string -} - -func (c *ChaosConfig) Validate() error { - if c.ExperimentCount == 0 { - return errors.New("experiment_count must be > 0") - } - - return nil -} - -type WaspConfig struct { - RPS int64 `toml:"rps"` - LPS int64 `toml:"lps"` - RateLimitUnitDuration time.Duration `toml:"rate_limit_unit_duration"` - Duration time.Duration `toml:"duration"` - CallTimeout time.Duration `toml:"call_timeout"` -} - -func (w *WaspConfig) Validate() error { - if w.RPS == 0 && w.LPS == 0 { - return errors.New("either RPS or LPS needs to be a positive integer") - } - if w.RPS != 0 && w.LPS != 0 { - return errors.New("only one of RPS or LPS can be set") - } - if w.Duration == 0 { - return errors.New("duration must be set and > 0") - } - if w.CallTimeout == 0 { - return errors.New("call_timeout must be set and > 0") - } - if w.RateLimitUnitDuration == 0 { - return errors.New("rate_limit_unit_duration must be set and > 0") - } - - return nil -} - // consistency test with no network disruptions with approximate emission of 1500-1600 logs per second for ~110-120 seconds // 6 filters are registered diff --git a/devenv/tests/vrfv2/batch_test.go b/devenv/tests/vrfv2/batch_test.go new file mode 100644 index 00000000000..48527263484 --- /dev/null +++ b/devenv/tests/vrfv2/batch_test.go @@ -0,0 +1,223 @@ +package vrfv2 + +import ( + "fmt" + "net/http" + "strconv" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + + de "github.com/smartcontractkit/chainlink/devenv" + "github.com/smartcontractkit/chainlink/devenv/contracts" + "github.com/smartcontractkit/chainlink/devenv/products" + productvrfv2 "github.com/smartcontractkit/chainlink/devenv/products/vrfv2" +) + +func TestVRFv2BatchFulfillmentEnabledDisabled(t *testing.T) { + t.Cleanup(func() { + _, cErr := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) + require.NoError(t, cErr, "failed to save container logs") + }) + + outputFile := "../../env-vrfv2-out.toml" + in, err := de.LoadOutput[de.Cfg](outputFile) + require.NoError(t, err, "failed to load devenv env-out from %s", outputFile) + + cfg, err := products.LoadOutput[productvrfv2.Configurator](outputFile) + require.NoError(t, err, "failed to load vrfv2 product config from env-out") + c := cfg.Config[0] + + keyHash := mustKeyHash(c) + chainID, err := strconv.ParseUint(in.Blockchains[0].Out.ChainID, 10, 64) + require.NoError(t, err, "failed to parse chain ID from env-out") + ctx := t.Context() + chainClient, err := products.InitSeth(in.Blockchains[0].Out.Nodes[0].ExternalWSUrl, []string{products.NetworkPrivateKey()}, &chainID) + require.NoError(t, err, "failed to init Seth client") + + coord, err := contracts.LoadVRFCoordinatorV2(chainClient, c.DeployedContracts.Coordinator) + require.NoError(t, err, "failed to load VRF coordinator v2") + batchCoordAddr := c.DeployedContracts.BatchCoordinator + linkToken, err := contracts.LoadLinkTokenContract(framework.L, chainClient, common.HexToAddress(c.DeployedContracts.LinkToken)) + require.NoError(t, err, "failed to load LINK token") + + cl, err := clclient.New(in.NodeSets[0].Out.CLNodes) + require.NoError(t, err, "failed to connect to Chainlink nodes") + + callbackGas := c.BatchCallbackGasLimit + if callbackGas == 0 { + callbackGas = 500_000 + } + batchBudget := c.BatchTxGasBudget + if batchBudget == 0 { + batchBudget = c.MaxGasLimitCoordinator + 400_000 + } + randRequestCountU32 := (batchBudget / callbackGas) - 1 + require.Greater(t, randRequestCountU32, uint32(1), "batch test needs randRequestCount > 1 (check batch budget vs callback gas)") + require.LessOrEqual(t, randRequestCountU32, uint32(^uint16(0)), "randRequestCount must fit in uint16") + randRequestCount := uint16(randRequestCountU32) //nolint:gosec // bounded by require.LessOrEqual to max uint16 + + fulfillTimeout := parseFulfillTimeout(c.RandomWordsFulfilledEventTimeout) + + pollPeriod, _ := time.ParseDuration(c.VRFJobPollPeriod) + if pollPeriod <= 0 { + pollPeriod = time.Second + } + requestTimeout, _ := time.ParseDuration(c.VRFJobRequestTimeout) + if requestTimeout <= 0 { + requestTimeout = 24 * time.Hour + } + + buildPipeline := func() (string, error) { + ps := &productvrfv2.TxPipelineSpec{ + Address: coord.Address(), + EstimateGasMultiplier: c.VRFJobEstimateGasMultiplier, + FromAddress: c.VRFKeyData.TxKeyAddresses[0], + } + if c.VRFJobSimulationBlock != "" { + sb := c.VRFJobSimulationBlock + ps.SimulationBlock = &sb + } + return ps.String() + } + + currentJobID := c.VRFKeyData.VRFJobID + switchJob := func(t *testing.T, batchOn bool) { + t.Helper() + if currentJobID != "" { + resp, dErr := cl[0].DeleteJob(currentJobID) + require.NoError(t, dErr, "failed to delete existing VRF job before switch") + require.Equal(t, http.StatusNoContent, resp.StatusCode, "delete job should return 204, got %d", resp.StatusCode) + } + obs, oErr := buildPipeline() + require.NoError(t, oErr, "failed to build observation pipeline spec") + gasMult := c.VRFJobBatchFulfillmentGasMultiplier + if gasMult == 0 { + gasMult = 1.1 + } + prefix := "enabled" + if !batchOn { + prefix = "disabled" + } + spec := &productvrfv2.JobSpec{ + Name: fmt.Sprintf("vrf-v2-batch-%s-%s", prefix, uuid.NewString()), + CoordinatorAddress: coord.Address(), + BatchCoordinatorAddress: batchCoordAddr, + PublicKey: c.VRFKeyData.PubKeyCompressed, + ExternalJobID: uuid.New().String(), + ObservationSource: obs, + MinIncomingConfirmations: int(c.MinimumConfirmations), + FromAddresses: c.VRFKeyData.TxKeyAddresses, + EVMChainID: in.Blockchains[0].Out.ChainID, + ForwardingAllowed: c.VRFJobForwardingAllowed, + BatchFulfillmentEnabled: batchOn, + BatchFulfillmentGasMultiplier: gasMult, + BackOffInitialDelay: 15 * time.Second, + BackOffMaxDelay: 5 * time.Minute, + PollPeriod: pollPeriod, + RequestTimeout: requestTimeout, + } + job, jErr := cl[0].MustCreateJob(spec) + require.NoError(t, jErr, "failed to create VRF job (batch=%v)", batchOn) + currentJobID = job.Data.ID + } + t.Cleanup(func() { + if currentJobID != "" { + _, _ = cl[0].DeleteJob(currentJobID) + } + }) + + t.Run("Batch Fulfillment Enabled", func(t *testing.T) { + require.NoError(t, deleteAllJobs(cl[0]), "failed to clear jobs before batch-enabled run") + currentJobID = "" + switchJob(t, true) + + consumers, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, c.SubFundingAmountLink, 1, 1) + require.NoError(t, err, "error setting up new consumers and subs") + subID := subIDs[0] + + _, fulfilled, err := requestRandomnessAndWaitForFulfillment(ctx, consumers[0], coord, keyHash, subID, + c.MinimumConfirmations, callbackGas, c.NumberOfWords, + randRequestCount, c.RandomnessRequestCountPerRequestDeviation, + fulfillTimeout, 0) + require.NoError(t, err, "error requesting randomness and waiting for fulfillment (batch on)") + + var wg sync.WaitGroup + wg.Add(1) + _, _, err = waitForRequestCountEqualToFulfillmentCount(ctx, consumers[0], 2*time.Minute, &wg) + require.NoError(t, err, "consumer request/fulfillment counts did not converge (batch on)") + wg.Wait() + + txs, _, err := cl[0].ReadTransactions() + require.NoError(t, err, "error reading node transactions") + var batchTxs []string + for _, tx := range txs.Data { + if stringsEqualFoldAddr(tx.Attributes.To, batchCoordAddr) { + batchTxs = append(batchTxs, tx.Attributes.Hash) + } + } + require.Len(t, batchTxs, 1, "expected exactly one tx from node to batch coordinator") + + fulfillTx, _, err := chainClient.Client.TransactionByHash(ctx, fulfilled.Raw.TxHash) + require.NoError(t, err, "failed to load fulfillment transaction") + require.NotNil(t, fulfillTx.To(), "fulfillment tx must have a To address") + require.True(t, stringsEqualFoldAddr(fulfillTx.To().Hex(), batchCoordAddr), + "fulfillment should go to batch coordinator %s, got %s", batchCoordAddr, fulfillTx.To().Hex()) + + receipt, err := chainClient.Client.TransactionReceipt(ctx, fulfillTx.Hash()) + require.NoError(t, err, "failed to load fulfillment receipt") + logs, err := contracts.ParseRandomWordsFulfilledLogs(coord, receipt.Logs) + require.NoError(t, err, "failed to parse RandomWordsFulfilled logs from receipt") + require.Len(t, logs, int(randRequestCount), "expected %d RandomWordsFulfilled logs in batch receipt", randRequestCount) + }) + + t.Run("Batch Fulfillment Disabled", func(t *testing.T) { + require.NoError(t, deleteAllJobs(cl[0]), "failed to clear jobs before batch-disabled run") + currentJobID = "" + switchJob(t, false) + + consumers, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, c.SubFundingAmountLink, 1, 1) + require.NoError(t, err, "error setting up new consumers and subs") + subID := subIDs[0] + + _, fulfilled, err := requestRandomnessAndWaitForFulfillment(ctx, consumers[0], coord, keyHash, subID, + c.MinimumConfirmations, callbackGas, c.NumberOfWords, + randRequestCount, c.RandomnessRequestCountPerRequestDeviation, + fulfillTimeout, 0) + require.NoError(t, err, "error requesting randomness and waiting for fulfillment (batch off)") + + var wg sync.WaitGroup + wg.Add(1) + _, _, err = waitForRequestCountEqualToFulfillmentCount(ctx, consumers[0], 2*time.Minute, &wg) + require.NoError(t, err, "consumer request/fulfillment counts did not converge (batch off)") + wg.Wait() + + fulfillTx, _, err := chainClient.Client.TransactionByHash(ctx, fulfilled.Raw.TxHash) + require.NoError(t, err, "failed to load fulfillment transaction") + require.NotNil(t, fulfillTx.To(), "fulfillment tx must have a To address") + require.True(t, stringsEqualFoldAddr(fulfillTx.To().Hex(), coord.Address()), + "fulfillment should go to coordinator %s, got %s", coord.Address(), fulfillTx.To().Hex()) + + txs, _, err := cl[0].ReadTransactions() + require.NoError(t, err, "error reading node transactions") + var coordTxs int + for _, tx := range txs.Data { + if stringsEqualFoldAddr(tx.Attributes.To, coord.Address()) { + coordTxs++ + } + } + require.Equal(t, int(randRequestCount), coordTxs, + "expected %d txs from node to coordinator (one per word path), got %d", randRequestCount, coordTxs) + }) +} + +func stringsEqualFoldAddr(a, b string) bool { + return common.HexToAddress(a) == common.HexToAddress(b) +} diff --git a/devenv/tests/vrfv2/bhs_test.go b/devenv/tests/vrfv2/bhs_test.go new file mode 100644 index 00000000000..1979e16db8d --- /dev/null +++ b/devenv/tests/vrfv2/bhs_test.go @@ -0,0 +1,194 @@ +package vrfv2 + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/onsi/gomega" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/blockhash_store" + "github.com/smartcontractkit/chainlink-evm/pkg/utils" + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + + de "github.com/smartcontractkit/chainlink/devenv" + "github.com/smartcontractkit/chainlink/devenv/contracts" + "github.com/smartcontractkit/chainlink/devenv/products" + productvrfv2 "github.com/smartcontractkit/chainlink/devenv/products/vrfv2" +) + +func TestVRFV2WithBHS(t *testing.T) { + t.Cleanup(func() { + _, cErr := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) + require.NoError(t, cErr, "failed to save container logs") + }) + + outputFile := "../../env-vrfv2-bhs-out.toml" + in, err := de.LoadOutput[de.Cfg](outputFile) + require.NoError(t, err, "failed to load devenv env-out from %s", outputFile) + + cfg, err := products.LoadOutput[productvrfv2.Configurator](outputFile) + require.NoError(t, err, "failed to load vrfv2 product config from env-out") + c := cfg.Config[0] + require.True(t, c.EnableBHSJob, "BHS product config must enable BHS job") + require.NotEmpty(t, c.VRFKeyData.BHSJobID, "BHS job ID must be set in env-out") + + keyHash := mustKeyHash(c) + chainID, err := strconv.ParseUint(in.Blockchains[0].Out.ChainID, 10, 64) + require.NoError(t, err, "failed to parse chain ID from env-out") + ctx := t.Context() + chainClient, err := products.InitSeth(in.Blockchains[0].Out.Nodes[0].ExternalWSUrl, []string{products.NetworkPrivateKey()}, &chainID) + require.NoError(t, err, "failed to init Seth client") + + coord, err := contracts.LoadVRFCoordinatorV2(chainClient, c.DeployedContracts.Coordinator) + require.NoError(t, err, "failed to load VRF coordinator v2") + linkToken, err := contracts.LoadLinkTokenContract(framework.L, chainClient, common.HexToAddress(c.DeployedContracts.LinkToken)) + require.NoError(t, err, "failed to load LINK token") + bhs, err := contracts.LoadBlockhashStore(chainClient, c.DeployedContracts.BHS) + require.NoError(t, err, "failed to load blockhash store") + + cl, err := clclient.New(in.NodeSets[0].Out.CLNodes) + require.NoError(t, err, "failed to connect to Chainlink nodes") + + t.Run("BHS Job with complete E2E - wait 256 blocks to see if Rand Request is fulfilled", func(t *testing.T) { + consumers, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, 0, 1, 1) + require.NoError(t, err, "error setting up new consumers and subs") + subID := subIDs[0] + + req, err := consumers[0].RequestRandomnessFromKey(coord, keyHash, subID, + c.MinimumConfirmations, c.CallbackGasLimit, c.NumberOfWords, + c.RandomnessRequestCountPerRequest, 0) + require.NoError(t, err, "error requesting randomness before long BHS wait") + reqBlock := req.Raw.BlockNumber + + // On EVM BLOCKHASH can no longer serve the original request block hash after ~256 blocks, so fulfillment path must depend on BHS-stored hash + products.WaitUntilChainHead(ctx, t, chainClient, reqBlock, c.BHSJobWaitBlocks+256, chainID, 5*time.Minute) + + var storedHash [32]byte + gomega.NewGomegaWithT(t).Eventually(func() bool { + hash, hErr := bhs.GetBlockhash(ctx, reqBlock) + if hErr != nil { + return false + } + storedHash = hash + return true + }, 3*time.Minute, time.Second).Should(gomega.BeTrue(), + "BHS should store blockhash for request block %d before funding", reqBlock) + require.Equal(t, 0, req.Raw.BlockHash.Cmp(common.BytesToHash(storedHash[:])), + "BHS stored blockhash should match RandomWordsRequested blockhash") + + amount := products.EtherToWei(big.NewFloat(c.SubFundingAmountLink)) + enc, err := utils.ABIEncode(`[{"type":"uint64"}]`, subID) + require.NoError(t, err, "error ABI-encoding sub ID for funding") + _, err = linkToken.TransferAndCall(coord.Address(), amount, enc) + require.NoError(t, err, "error funding subscription after BHS wait") + + fulfillTimeout := parseFulfillTimeout(c.RandomWordsFulfilledEventTimeout) + _, err = contracts.WaitRandomWordsFulfilled(coord, req.RequestID, req.Raw.BlockNumber, fulfillTimeout) + require.NoError(t, err, "RandomWordsFulfilled not seen after funding sub (BHS E2E)") + }) + + t.Run("BHS Job should fill in blockhashes into BHS contract for unfulfilled requests", func(t *testing.T) { + consumers, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, 0, 1, 1) + require.NoError(t, err, "error setting up new consumers and subs") + subID := subIDs[0] + + req, err := consumers[0].RequestRandomnessFromKey(coord, keyHash, subID, + c.MinimumConfirmations, c.CallbackGasLimit, c.NumberOfWords, + c.RandomnessRequestCountPerRequest, 0) + require.NoError(t, err, "error requesting randomness for BHS store test") + reqBlock := req.Raw.BlockNumber + + _, err = bhs.GetBlockhash(ctx, reqBlock) + require.Error(t, err, "BHS should not have blockhash for request block immediately after request") + + blocks := c.BHSJobWaitBlocks + if blocks < 0 { + t.Fatalf("negative blocks: %d", blocks) + } + products.WaitUntilChainHead(ctx, t, chainClient, reqBlock, blocks, chainID, time.Minute) + + metrics, err := consumers[0].GetLoadTestMetrics(ctx) + require.NoError(t, err, "error reading consumer load test metrics") + require.Equal(t, 0, metrics.RequestCount.Cmp(big.NewInt(1)), "expected exactly one randomness request on consumer") + require.Equal(t, 0, metrics.FulfilmentCount.Cmp(big.NewInt(0)), "expected no fulfillment yet while request is unfulfilled") + + gom := gomega.NewGomegaWithT(t) + var bhsNodeTxHash string + gom.Eventually(func(g gomega.Gomega) { + txs, _, rErr := cl[1].ReadTransactions() + g.Expect(rErr).ShouldNot(gomega.HaveOccurred()) + h, ok := findBHSStoreTxHashForBlock(txs.Data, c.DeployedContracts.BHS, reqBlock) + g.Expect(ok).Should(gomega.BeTrue(), "expected BHS node tx storing block %d (got %d node txs)", reqBlock, len(txs.Data)) + bhsNodeTxHash = h + }, "2m", "1s").Should(gomega.Succeed()) + + tx, _, err := chainClient.Client.TransactionByHash(ctx, common.HexToHash(bhsNodeTxHash)) + require.NoError(t, err, "failed to load BHS node store transaction") + storedBlock, err := decodeBHSStoreBlockNumber(tx.Data()) + require.NoError(t, err, "failed to decode store block number from BHS tx calldata") + require.Equal(t, reqBlock, storedBlock, "BHS store tx should target request block %d", reqBlock) + + var storedHash [32]byte + gom.Eventually(func(g gomega.Gomega) { + h, hErr := bhs.GetBlockhash(ctx, reqBlock) + g.Expect(hErr).ShouldNot(gomega.HaveOccurred()) + storedHash = h + }, "2m", "1s").Should(gomega.Succeed()) + require.Equal(t, 0, req.Raw.BlockHash.Cmp(common.BytesToHash(storedHash[:])), + "blockhash stored in BHS must match chain block hash at request block") + }) +} + +func decodeBHSStoreBlockNumber(data []byte) (uint64, error) { + parsed, err := blockhash_store.BlockhashStoreMetaData.GetAbi() + if err != nil { + return 0, err + } + if len(data) < 4 { + return 0, errors.New("short calldata") + } + m, err := parsed.MethodById(data[:4]) + if err != nil { + return 0, err + } + args, err := m.Inputs.Unpack(data[4:]) + if err != nil { + return 0, err + } + if len(args) != 1 { + return 0, fmt.Errorf("expected 1 arg, got %d", len(args)) + } + bn, ok := args[0].(*big.Int) + if !ok { + return 0, errors.New("expected *big.Int") + } + return bn.Uint64(), nil +} + +// findBHSStoreTxHashForBlock returns the hash of a finalized node tx that calls BHS store for wantBlock. +// With fast chains the node may list multiple BHS store txs; we match by decoded calldata block number. +func findBHSStoreTxHashForBlock(txs []clclient.TransactionData, bhsAddr string, wantBlock uint64) (txHash string, ok bool) { + for _, tx := range txs { + if !strings.EqualFold(tx.Attributes.To, bhsAddr) { + continue + } + data := common.FromHex(tx.Attributes.Data) + if len(data) < 4 { + continue + } + stored, err := decodeBHSStoreBlockNumber(data) + if err != nil || stored != wantBlock { + continue + } + return tx.Attributes.Hash, true + } + return "", false +} diff --git a/devenv/tests/vrfv2/helpers.go b/devenv/tests/vrfv2/helpers.go new file mode 100644 index 00000000000..7c128eb3294 --- /dev/null +++ b/devenv/tests/vrfv2/helpers.go @@ -0,0 +1,187 @@ +package vrfv2 + +import ( + "context" + "fmt" + "math/big" + "net/http" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/smartcontractkit/chainlink-evm/pkg/utils" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + "github.com/smartcontractkit/chainlink-testing-framework/seth" + + "github.com/smartcontractkit/chainlink/devenv/contracts" + "github.com/smartcontractkit/chainlink/devenv/products" + productvrfv2 "github.com/smartcontractkit/chainlink/devenv/products/vrfv2" +) + +func deployConsumersAndFundSubs( + ctx context.Context, + chainClient *seth.Client, + coord *contracts.EthereumVRFCoordinatorV2, + link *contracts.EthereumLinkToken, + subFundingLink float64, + numConsumers, numSubs int, +) ([]*contracts.EthereumVRFv2LoadTestConsumer, []uint64, error) { + consumers := make([]*contracts.EthereumVRFv2LoadTestConsumer, 0, numConsumers) + for range numConsumers { + c, err := contracts.DeployVRFv2LoadTestConsumer(chainClient, coord.Address()) + if err != nil { + return nil, nil, err + } + consumers = append(consumers, c) + } + subIDs := make([]uint64, 0, numSubs) + for range numSubs { + receipt, err := coord.CreateSubscription() + if err != nil { + return nil, nil, err + } + subID, err := contracts.FindVRFv2SubscriptionID(receipt) + if err != nil { + return nil, nil, err + } + subIDs = append(subIDs, subID) + } + amount := products.EtherToWei(big.NewFloat(subFundingLink)) + for _, subID := range subIDs { + enc, err := utils.ABIEncode(`[{"type":"uint64"}]`, subID) + if err != nil { + return nil, nil, err + } + if _, err := link.TransferAndCall(coord.Address(), amount, enc); err != nil { + return nil, nil, err + } + } + for _, subID := range subIDs { + for _, c := range consumers { + if err := coord.AddConsumer(subID, c.Address()); err != nil { + return nil, nil, err + } + } + } + return consumers, subIDs, nil +} + +func requestRandomnessAndWaitForFulfillment( + ctx context.Context, + consumer *contracts.EthereumVRFv2LoadTestConsumer, + coord *contracts.EthereumVRFCoordinatorV2, + keyHash [32]byte, + subID uint64, + minConf uint16, + callbackGasLimit, numWords uint32, + reqPerReq, reqDev uint16, + fulfillTimeout time.Duration, + keyNum int, +) (*contracts.CoordinatorRandomWordsRequested, *contracts.CoordinatorRandomWordsFulfilled, error) { + req, err := consumer.RequestRandomnessFromKey(coord, keyHash, subID, minConf, callbackGasLimit, numWords, reqPerReq, keyNum) + if err != nil { + return nil, nil, err + } + fulfilled, err := contracts.WaitRandomWordsFulfilled(coord, req.RequestID, req.Raw.BlockNumber, fulfillTimeout) + if err != nil { + return req, nil, err + } + return req, fulfilled, nil +} + +func directFundingRequestAndWait( + ctx context.Context, + consumer *contracts.EthereumVRFV2WrapperLoadTestConsumer, + coord *contracts.EthereumVRFCoordinatorV2, + subID uint64, + minConf uint16, + callbackGasLimit, numWords uint32, + reqPerReq uint16, + fulfillTimeout time.Duration, +) (*contracts.CoordinatorRandomWordsFulfilled, error) { + req, err := consumer.RequestRandomness(coord, minConf, callbackGasLimit, numWords, reqPerReq) + if err != nil { + return nil, err + } + return contracts.WaitRandomWordsFulfilled(coord, req.RequestID, req.Raw.BlockNumber, fulfillTimeout) +} + +func waitForRequestCountEqualToFulfillmentCount( + ctx context.Context, + consumer *contracts.EthereumVRFv2LoadTestConsumer, + timeout time.Duration, + wg *sync.WaitGroup, +) (reqCount *big.Int, fulCount *big.Int, err error) { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + deadline := time.Now().Add(timeout) + for { + select { + case <-ctx.Done(): + wg.Done() + return reqCount, fulCount, ctx.Err() + case <-ticker.C: + m, mErr := consumer.GetLoadTestMetrics(ctx) + if mErr != nil { + wg.Done() + return nil, nil, mErr + } + reqCount, fulCount = m.RequestCount, m.FulfilmentCount + if m.RequestCount.Cmp(m.FulfilmentCount) == 0 && m.RequestCount.Sign() > 0 { + wg.Done() + return m.RequestCount, m.FulfilmentCount, nil + } + if time.Now().After(deadline) { + wg.Done() + return reqCount, fulCount, fmt.Errorf("timeout waiting request==fulfillment counts (req=%s ful=%s)", + reqCount.String(), fulCount.String()) + } + } + } +} + +func deleteAllJobs(node *clclient.ChainlinkClient) error { + jobs, _, err := node.ReadJobs() + if err != nil { + return err + } + for _, m := range jobs.Data { + id, ok := m["id"].(string) + if !ok { + return fmt.Errorf("job missing id: %+v", m) + } + resp, err := node.DeleteJob(id) + if err != nil { + return err + } + if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { + return fmt.Errorf("delete job %s: status %d", id, resp.StatusCode) + } + } + return nil +} + +func getTxFromAddress(tx *types.Transaction) (string, error) { + from, err := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx) + if err != nil { + return "", err + } + return from.Hex(), nil +} + +func parseFulfillTimeout(s string) time.Duration { + d, err := time.ParseDuration(s) + if err != nil || d <= 0 { + return 2 * time.Minute + } + return d +} + +func mustKeyHash(c *productvrfv2.VRFv2) [32]byte { + h := common.HexToHash(c.VRFKeyData.KeyHash) + var out [32]byte + copy(out[:], h[:]) + return out +} diff --git a/devenv/tests/vrfv2/multiple_keys_test.go b/devenv/tests/vrfv2/multiple_keys_test.go new file mode 100644 index 00000000000..9485d8c23a9 --- /dev/null +++ b/devenv/tests/vrfv2/multiple_keys_test.go @@ -0,0 +1,87 @@ +package vrfv2 + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + + de "github.com/smartcontractkit/chainlink/devenv" + "github.com/smartcontractkit/chainlink/devenv/contracts" + "github.com/smartcontractkit/chainlink/devenv/products" + productvrfv2 "github.com/smartcontractkit/chainlink/devenv/products/vrfv2" +) + +func TestVRFv2MultipleSendingKeys(t *testing.T) { + t.Cleanup(func() { + _, cErr := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) + require.NoError(t, cErr, "failed to save container logs") + }) + + outputFile := "../../env-vrfv2-out.toml" + in, err := de.LoadOutput[de.Cfg](outputFile) + require.NoError(t, err, "failed to load devenv env-out from %s", outputFile) + + cfg, err := products.LoadOutput[productvrfv2.Configurator](outputFile) + require.NoError(t, err, "failed to load vrfv2 product config from env-out") + c := cfg.Config[0] + require.GreaterOrEqual(t, c.NumTxKeys, 2, "two_keys.toml should set num_tx_keys >= 2 for this test") + + keyHash := mustKeyHash(c) + chainID, err := strconv.ParseUint(in.Blockchains[0].Out.ChainID, 10, 64) + require.NoError(t, err, "failed to parse chain ID from env-out") + ctx := t.Context() + chainClient, err := products.InitSeth(in.Blockchains[0].Out.Nodes[0].ExternalWSUrl, []string{products.NetworkPrivateKey()}, &chainID) + require.NoError(t, err, "failed to init Seth client") + + coord, err := contracts.LoadVRFCoordinatorV2(chainClient, c.DeployedContracts.Coordinator) + require.NoError(t, err, "failed to load VRF coordinator v2") + linkToken, err := contracts.LoadLinkTokenContract(framework.L, chainClient, common.HexToAddress(c.DeployedContracts.LinkToken)) + require.NoError(t, err, "failed to load LINK token") + + cl, err := clclient.New(in.NodeSets[0].Out.CLNodes) + require.NoError(t, err, "failed to connect to Chainlink nodes") + + t.Run("Request Randomness with multiple sending keys", func(t *testing.T) { + consumers, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, c.SubFundingAmountLink, 1, 1) + require.NoError(t, err, "error setting up new consumers and subs") + subID := subIDs[0] + + txKeys, _, err := cl[0].ReadTxKeys("evm") + require.NoError(t, err, "error reading node TX keys") + require.Len(t, txKeys.Data, c.NumTxKeys+1, "expected 1 primary + num_tx_keys EVM keys on node") + + fulfillTimeout := parseFulfillTimeout(c.RandomWordsFulfilledEventTimeout) + // Match legacy smoke/vrfv2_test.go: always use Seth key 0 for consumer requests. + // The assertion is on fulfillment tx senders — the VRF job rotates node fromAddresses. + var fromAddrs []string + for range c.NumTxKeys + 1 { + _, fulfilled, err := requestRandomnessAndWaitForFulfillment(ctx, consumers[0], coord, keyHash, subID, + c.MinimumConfirmations, c.CallbackGasLimit, c.NumberOfWords, + c.RandomnessRequestCountPerRequest, c.RandomnessRequestCountPerRequestDeviation, + fulfillTimeout, 0) + require.NoError(t, err, "error requesting randomness and waiting for fulfilment") + tx, _, err := chainClient.Client.TransactionByHash(ctx, fulfilled.Raw.TxHash) + require.NoError(t, err, "failed to load fulfillment transaction") + from, err := getTxFromAddress(tx) + require.NoError(t, err, "failed to parse fulfillment tx sender") + fromAddrs = append(fromAddrs, strings.ToLower(from)) + } + + var keyAddrs []string + for _, k := range txKeys.Data { + keyAddrs = append(keyAddrs, strings.ToLower(k.Attributes.Address)) + } + less := func(a, b string) bool { return a < b } + require.Empty(t, cmp.Diff(keyAddrs, fromAddrs, cmpopts.SortSlices(less)), + "fulfillment tx senders should match node TX keys (sorted): diff should be empty") + }) +} diff --git a/devenv/tests/vrfv2/smoke_test.go b/devenv/tests/vrfv2/smoke_test.go new file mode 100644 index 00000000000..9f97c86bb0a --- /dev/null +++ b/devenv/tests/vrfv2/smoke_test.go @@ -0,0 +1,291 @@ +package vrfv2 + +import ( + "fmt" + "math/big" + "strconv" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + commonassets "github.com/smartcontractkit/chainlink-common/pkg/assets" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + + "github.com/smartcontractkit/chainlink-evm/pkg/utils" + de "github.com/smartcontractkit/chainlink/devenv" + "github.com/smartcontractkit/chainlink/devenv/contracts" + "github.com/smartcontractkit/chainlink/devenv/products" + productvrfv2 "github.com/smartcontractkit/chainlink/devenv/products/vrfv2" +) + +func TestVRFv2Basic(t *testing.T) { + t.Cleanup(func() { + _, cErr := framework.SaveContainerLogs(fmt.Sprintf("%s-%s", framework.DefaultCTFLogsDir, t.Name())) + require.NoError(t, cErr, "failed to save container logs") + }) + + outputFile := "../../env-vrfv2-out.toml" + in, err := de.LoadOutput[de.Cfg](outputFile) + require.NoError(t, err, "failed to load devenv env-out from %s", outputFile) + + cfg, err := products.LoadOutput[productvrfv2.Configurator](outputFile) + require.NoError(t, err, "failed to load vrfv2 product config from env-out") + require.NotEmpty(t, cfg.Config, "vrfv2 config must not be empty in env-out") + c := cfg.Config[0] + + // Unlike vrfv2plus smoke (reconcileConfiguredFunding), we do not top up between + // subtests: each subtest deploys fresh load-test consumers and funded subs; Direct + // Funding is self-contained. Node TX keys are funded once in ConfigureJobsAndContracts. + + keyHash := mustKeyHash(c) + + chainID, err := strconv.ParseUint(in.Blockchains[0].Out.ChainID, 10, 64) + require.NoError(t, err, "failed to parse chain ID from env-out") + bcNode := in.Blockchains[0].Out.Nodes[0] + ctx := t.Context() + chainClient, err := products.InitSeth(bcNode.ExternalWSUrl, []string{products.NetworkPrivateKey()}, &chainID) + require.NoError(t, err, "failed to init Seth client") + + coord, err := contracts.LoadVRFCoordinatorV2(chainClient, c.DeployedContracts.Coordinator) + require.NoError(t, err, "failed to load VRF coordinator v2") + + linkToken, err := contracts.LoadLinkTokenContract(framework.L, chainClient, common.HexToAddress(c.DeployedContracts.LinkToken)) + require.NoError(t, err, "failed to load LINK token") + + cl, err := clclient.New(in.NodeSets[0].Out.CLNodes) + require.NoError(t, err, "failed to connect to Chainlink nodes") + + fulfillTimeout := parseFulfillTimeout(c.RandomWordsFulfilledEventTimeout) + + t.Run("Request Randomness", func(t *testing.T) { + consumers, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, c.SubFundingAmountLink, 1, 1) + require.NoError(t, err, "error setting up new consumers and subs") + subID := subIDs[0] + sub, err := coord.GetSubscription(ctx, subID) + require.NoError(t, err, "error getting subscription information before request") + balBefore := new(big.Int).Set(sub.Balance) + + _, fulfilled, err := requestRandomnessAndWaitForFulfillment(ctx, consumers[0], coord, keyHash, subID, + c.MinimumConfirmations, c.CallbackGasLimit, c.NumberOfWords, + c.RandomnessRequestCountPerRequest, c.RandomnessRequestCountPerRequestDeviation, + fulfillTimeout, 0) + require.NoError(t, err, "error requesting randomness and waiting for fulfilment") + + expectedBal := new(big.Int).Sub(balBefore, fulfilled.Payment) + subAfter, err := coord.GetSubscription(ctx, subID) + require.NoError(t, err, "error getting subscription after fulfillment") + require.Equal(t, 0, expectedBal.Cmp(subAfter.Balance), + "subscription LINK balance should equal pre-fulfillment balance minus payment (expected %s got %s)", expectedBal.String(), subAfter.Balance.String()) + + status, err := consumers[0].GetRequestStatus(ctx, fulfilled.RequestID) + require.NoError(t, err, "error getting randomness request status") + require.True(t, status.Fulfilled, "random words request should be fulfilled") + require.Len(t, status.RandomWords, int(c.NumberOfWords), "wrong number of random words in consumer status") + for i, w := range status.RandomWords { + require.Equal(t, 1, w.Cmp(big.NewInt(0)), "random word %d should be non-zero", i) + } + }) + + t.Run("VRF Node waits block confirmation number specified by the consumer before sending fulfilment on-chain", func(t *testing.T) { + consumers, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, c.SubFundingAmountLink, 1, 1) + require.NoError(t, err, "error setting up new consumers and subs") + subID := subIDs[0] + + const expectedBlockWait = uint16(10) + req, fulfilled, err := requestRandomnessAndWaitForFulfillment(ctx, consumers[0], coord, keyHash, subID, + expectedBlockWait, c.CallbackGasLimit, c.NumberOfWords, + c.RandomnessRequestCountPerRequest, c.RandomnessRequestCountPerRequestDeviation, + fulfillTimeout, 0) + require.NoError(t, err, "error requesting randomness and waiting for fulfilment") + require.GreaterOrEqual(t, fulfilled.Raw.BlockNumber, req.Raw.BlockNumber+uint64(expectedBlockWait), + "fulfillment block should be at least request block + minimum confirmations (req=%d fulfilled=%d minConf=%d)", + req.Raw.BlockNumber, fulfilled.Raw.BlockNumber, expectedBlockWait) + }) + + t.Run("CL Node VRF Job Runs", func(t *testing.T) { + runsBefore, err := cl[0].MustReadRunsByJob(c.VRFKeyData.VRFJobID) + require.NoError(t, err, "failed to read VRF job runs before request") + beforeN := len(runsBefore.Data) + + consumers, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, c.SubFundingAmountLink, 1, 1) + require.NoError(t, err, "error setting up new consumers and subs") + subID := subIDs[0] + + _, _, err = requestRandomnessAndWaitForFulfillment(ctx, consumers[0], coord, keyHash, subID, + c.MinimumConfirmations, c.CallbackGasLimit, c.NumberOfWords, + c.RandomnessRequestCountPerRequest, c.RandomnessRequestCountPerRequestDeviation, + fulfillTimeout, 0) + require.NoError(t, err, "error requesting randomness and waiting for fulfilment") + + runsAfter, err := cl[0].MustReadRunsByJob(c.VRFKeyData.VRFJobID) + require.NoError(t, err, "failed to read VRF job runs after request") + require.Len(t, runsAfter.Data, beforeN+1, "VRF job should have one new run after fulfillment (before=%d after=%d)", beforeN, len(runsAfter.Data)) + }) + + t.Run("Direct Funding", func(t *testing.T) { + wrapper, err := contracts.DeployVRFV2Wrapper(chainClient, + c.DeployedContracts.LinkToken, c.DeployedContracts.MockFeed, c.DeployedContracts.Coordinator) + require.NoError(t, err, "failed to deploy VRF v2 wrapper") + err = wrapper.SetConfig(c.WrapperGasOverhead, c.CoordinatorGasOverhead, c.WrapperPremiumPercentage, keyHash, c.WrapperMaxNumberOfWords) + require.NoError(t, err, "failed to set wrapper config") + wrapperSubID, err := wrapper.GetSubID(ctx) + require.NoError(t, err, "failed to read wrapper subscription ID") + + // Fund the wrapper's coordinator subscription with LINK via the coordinator + // (same as integration-tests FundSubscriptionWithLink — not TransferAndCall to the wrapper). + amount := products.EtherToWei(big.NewFloat(c.SubFundingAmountLink)) + enc, err := utilsABIEncodeUint64(wrapperSubID) + require.NoError(t, err, "failed to ABI-encode wrapper sub ID for funding") + _, err = linkToken.TransferAndCall(coord.Address(), amount, enc) + require.NoError(t, err, "failed to fund wrapper subscription with LINK via coordinator") + + wConsumer, err := contracts.DeployVRFV2WrapperLoadTestConsumer(chainClient, c.DeployedContracts.LinkToken, wrapper.Address()) + require.NoError(t, err, "failed to deploy wrapper load test consumer") + fundJuels := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(int64(c.WrapperConsumerFundingAmountLink))) + err = linkToken.Transfer(wConsumer.Address(), fundJuels) + require.NoError(t, err, "failed to fund wrapper consumer with LINK") + + balBefore, err := linkToken.BalanceOf(ctx, wConsumer.Address()) + require.NoError(t, err, "failed to read wrapper consumer LINK balance before request") + wSub, err := coord.GetSubscription(ctx, wrapperSubID) + require.NoError(t, err, "failed to get wrapper subscription before request") + subBalBefore := new(big.Int).Set(wSub.Balance) + + fulfilled, err := directFundingRequestAndWait(ctx, wConsumer, coord, wrapperSubID, + c.MinimumConfirmations, c.CallbackGasLimit, c.NumberOfWords, + c.RandomnessRequestCountPerRequest, fulfillTimeout) + require.NoError(t, err, "direct funding request did not fulfill in time") + + expSub := new(big.Int).Sub(subBalBefore, fulfilled.Payment) + wSubAfter, err := coord.GetSubscription(ctx, wrapperSubID) + require.NoError(t, err, "failed to get wrapper subscription after fulfillment") + require.Equal(t, 0, expSub.Cmp(wSubAfter.Balance), + "wrapper sub LINK balance should equal pre-fulfillment minus payment (expected %s got %s)", expSub.String(), wSubAfter.Balance.String()) + + consStatus, err := wConsumer.GetRequestStatus(ctx, fulfilled.RequestID) + require.NoError(t, err, "error getting wrapper consumer request status") + require.True(t, consStatus.Fulfilled, "direct funding request should be fulfilled") + balAfter, err := linkToken.BalanceOf(ctx, wConsumer.Address()) + require.NoError(t, err, "failed to read wrapper consumer LINK balance after request") + expConsumerBal := new(big.Int).Sub(balBefore, consStatus.Paid) + require.Equal(t, 0, expConsumerBal.Cmp(balAfter), + "consumer LINK balance should equal pre-request minus paid amount (expected %s got %s)", expConsumerBal.String(), balAfter.String()) + require.Len(t, consStatus.RandomWords, int(c.NumberOfWords), "wrong number of random words from direct funding") + for i, w := range consStatus.RandomWords { + require.Equal(t, 1, w.Cmp(big.NewInt(0)), "random word %d should be non-zero", i) + } + t.Logf("Consumer balance before %s after %s paid %s", + (*commonassets.Link)(balBefore).Link(), (*commonassets.Link)(balAfter).Link(), (*commonassets.Link)(consStatus.Paid).Link()) + }) + + t.Run("Oracle Withdraw", func(t *testing.T) { + tests.SkipFlakey(t, "https://smartcontract-it.atlassian.net/browse/DX-527") + + consumers, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, c.SubFundingAmountLink, 1, 1) + require.NoError(t, err, "error setting up new consumers and subs") + subID := subIDs[0] + + _, fulfilled, err := requestRandomnessAndWaitForFulfillment(ctx, consumers[0], coord, keyHash, subID, + c.MinimumConfirmations, c.CallbackGasLimit, c.NumberOfWords, + c.RandomnessRequestCountPerRequest, c.RandomnessRequestCountPerRequestDeviation, + fulfillTimeout, 0) + require.NoError(t, err, "error requesting randomness and waiting for fulfilment") + + root := chainClient.MustGetRootKeyAddress() + balBefore, err := linkToken.BalanceOf(ctx, root.Hex()) + require.NoError(t, err, "failed to read oracle LINK balance before withdraw") + err = coord.OracleWithdraw(root, fulfilled.Payment) + require.NoError(t, err, "oracle withdraw failed") + balAfter, err := linkToken.BalanceOf(ctx, root.Hex()) + require.NoError(t, err, "failed to read oracle LINK balance after withdraw") + require.Equal(t, 1, balAfter.Cmp(balBefore), "oracle LINK balance should increase after withdraw (before=%s after=%s)", balBefore.String(), balAfter.String()) + }) + + t.Run("Canceling Sub And Returning Funds", func(t *testing.T) { + _, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, c.SubFundingAmountLink, 1, 1) + require.NoError(t, err, "error setting up new consumers and subs") + subID := subIDs[0] + + toAddr, err := randomWalletAddress() + require.NoError(t, err, "failed to generate cancel recipient address") + balBefore, err := linkToken.BalanceOf(ctx, toAddr.Hex()) + require.NoError(t, err, "failed to read recipient LINK balance before cancel") + + sub, err := coord.GetSubscription(ctx, subID) + require.NoError(t, err, "error getting subscription before cancel") + subBal := new(big.Int).Set(sub.Balance) + require.Equal(t, 1, subBal.Sign(), "subscription should be funded before cancel (expected positive balance from SubFundingAmountLink); got %s", subBal.String()) + + _, cancelEv, err := coord.CancelSubscription(subID, toAddr) + require.NoError(t, err, "cancel subscription failed") + require.Equal(t, 0, subBal.Cmp(cancelEv.Amount), + "SubscriptionCanceled amount should match sub balance (subBal=%s canceled=%s)", subBal.String(), cancelEv.Amount.String()) + + _, err = coord.GetSubscription(ctx, subID) + require.Error(t, err, "get subscription should fail after cancel") + + balAfter, err := linkToken.BalanceOf(ctx, toAddr.Hex()) + require.NoError(t, err, "failed to read recipient LINK balance after cancel") + returned := new(big.Int).Sub(balAfter, balBefore) + require.Equal(t, 0, subBal.Cmp(returned), + "recipient should receive full sub LINK balance (expected return=%s got=%s)", subBal.String(), returned.String()) + }) + + t.Run("Owner Canceling Sub And Returning Funds While Having Pending Requests", func(t *testing.T) { + consumers, subIDs, err := deployConsumersAndFundSubs(ctx, chainClient, coord, linkToken, 0, 1, 1) + require.NoError(t, err, "error setting up unfunded consumer and sub") + subID := subIDs[0] + + pending, err := coord.PendingRequestsExist(ctx, subID) + require.NoError(t, err, "failed to check pending requests before stuck request") + require.False(t, pending, "should have no pending requests before underfunded request") + + _, _, err = requestRandomnessAndWaitForFulfillment(ctx, consumers[0], coord, keyHash, subID, + c.MinimumConfirmations, c.CallbackGasLimit, c.NumberOfWords, + c.RandomnessRequestCountPerRequest, c.RandomnessRequestCountPerRequestDeviation, + 5*time.Second, 0) + require.Error(t, err, "underfunded request should not fulfill within short timeout") + + pending, err = coord.PendingRequestsExist(ctx, subID) + require.NoError(t, err, "failed to check pending requests after stuck request") + require.True(t, pending, "pending request should exist before owner cancel") + + root := chainClient.MustGetRootKeyAddress() + wBalBefore, err := linkToken.BalanceOf(ctx, root.Hex()) + require.NoError(t, err, "failed to read owner LINK balance before owner cancel") + sub, err := coord.GetSubscription(ctx, subID) + require.NoError(t, err, "error getting subscription before owner cancel") + subBal := new(big.Int).Set(sub.Balance) + + _, cancelEv, err := coord.OwnerCancelSubscription(subID) + require.NoError(t, err, "owner cancel subscription failed") + require.Equal(t, 0, subBal.Cmp(cancelEv.Amount), + "owner cancel amount should match sub balance (subBal=%s canceled=%s)", subBal.String(), cancelEv.Amount.String()) + + _, err = coord.GetSubscription(ctx, subID) + require.Error(t, err, "get subscription should fail after owner cancel") + + wBalAfter, err := linkToken.BalanceOf(ctx, root.Hex()) + require.NoError(t, err, "failed to read owner LINK balance after owner cancel") + returned := new(big.Int).Sub(wBalAfter, wBalBefore) + require.Equal(t, 0, subBal.Cmp(returned), + "owner should receive full sub LINK balance (expected=%s got=%s)", subBal.String(), returned.String()) + }) +} + +func utilsABIEncodeUint64(subID uint64) ([]byte, error) { + return utils.ABIEncode(`[{"type":"uint64"}]`, subID) +} + +func randomWalletAddress() (common.Address, error) { + key, err := crypto.GenerateKey() + if err != nil { + return common.Address{}, err + } + return crypto.PubkeyToAddress(key.PublicKey), nil +} diff --git a/devenv/tests/vrfv2plus/bhf_test.go b/devenv/tests/vrfv2plus/bhf_test.go index 0f7cd142d78..8a8064715b4 100644 --- a/devenv/tests/vrfv2plus/bhf_test.go +++ b/devenv/tests/vrfv2plus/bhf_test.go @@ -93,7 +93,7 @@ func TestVRFV2PlusWithBHF(t *testing.T) { // Wait at least 257 blocks so the BHF job can store the blockhash and // the coordinator can verify it against the BatchBHS. - waitForBHSWindow(ctx, t, chainClient, requestBlock, 257, chainID, 5*time.Minute) + products.WaitUntilChainHead(ctx, t, chainClient, requestBlock, 257, chainID, 5*time.Minute) // Fund the subscription so the stuck request can be fulfilled. nativeWei := products.EtherToWei(big.NewFloat(c.SubFundingAmountNative)) diff --git a/devenv/tests/vrfv2plus/bhs_test.go b/devenv/tests/vrfv2plus/bhs_test.go index d12f9178fe9..95ce3db1766 100644 --- a/devenv/tests/vrfv2plus/bhs_test.go +++ b/devenv/tests/vrfv2plus/bhs_test.go @@ -1,7 +1,6 @@ package vrfv2plus import ( - "context" "fmt" "math/big" "strconv" @@ -12,10 +11,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/onsi/gomega" "github.com/stretchr/testify/require" - "golang.org/x/sync/errgroup" "github.com/smartcontractkit/chainlink-testing-framework/framework" - "github.com/smartcontractkit/chainlink-testing-framework/seth" de "github.com/smartcontractkit/chainlink/devenv" "github.com/smartcontractkit/chainlink/devenv/contracts" @@ -92,7 +89,7 @@ func TestVRFV2PlusWithBHS(t *testing.T) { _, qErr = bhs.GetBlockhash(ctx, requestBlock) require.Error(t, qErr, "blockhash should not exist in BHS immediately after request") - waitForBHSWindow(ctx, t, chainClient, requestBlock, c.BHSJobWaitBlocks, chainID, 10*time.Second) + products.WaitUntilChainHead(ctx, t, chainClient, requestBlock, c.BHSJobWaitBlocks+10, chainID, 10*time.Second) reqCount, cErr := consumer.RequestCount(ctx) require.NoError(t, cErr) @@ -139,7 +136,7 @@ func TestVRFV2PlusWithBHS(t *testing.T) { require.Positive(t, requestBlock, "request block must be non-zero") // On EVM BLOCKHASH can no longer serve the original request block hash after ~256 blocks, so fulfillment path must depend on BHS-stored hash - waitForBHSWindow(ctx, t, chainClient, requestBlock, c.BHSJobWaitBlocks+256, chainID, 5*time.Minute) + products.WaitUntilChainHead(ctx, t, chainClient, requestBlock, c.BHSJobWaitBlocks+256, chainID, 5*time.Minute) var storedHash [32]byte gomega.NewGomegaWithT(t).Eventually(func() bool { @@ -180,74 +177,3 @@ func TestVRFV2PlusWithBHS(t *testing.T) { require.True(t, fulfilledSuccess, "RandomWordsFulfilled.Success should be true") }) } - -func waitForBHSWindow( - ctx context.Context, - t *testing.T, - chainClient *seth.Client, - requestBlock uint64, - waitBlocks int, - chainID uint64, - timeout time.Duration, -) { - t.Helper() - require.GreaterOrEqual(t, waitBlocks, 0, "waitBlocks must be non-negative") - - targetBlock := requestBlock + uint64(waitBlocks) + 10 //nolint:gosec // waitBlocks is validated non-negative above - if chainID != 1337 { - gomega.NewGomegaWithT(t).Eventually(func() bool { - blk, err := chainClient.Client.BlockNumber(ctx) - if err != nil { - return false - } - return blk >= targetBlock - }, timeout, time.Second).Should(gomega.BeTrue(), - "timed out waiting for chain to reach block %d", targetBlock) - return - } - - counter, err := contracts.DeployCounterContract(chainClient) - require.NoError(t, err, "failed to deploy counter contract for tx-spam block advancement") - err = counter.Reset() - require.NoError(t, err, "failed to reset counter contract for tx-spam") - - waitCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - done := make(chan struct{}) - var eg errgroup.Group - eg.Go(func() error { - ticker := time.NewTicker(500 * time.Millisecond) - defer ticker.Stop() - for { - select { - case <-waitCtx.Done(): - return fmt.Errorf("timeout waiting for chain to reach block %d", targetBlock) - case <-ticker.C: - blk, bErr := chainClient.Client.BlockNumber(waitCtx) - if bErr != nil { - continue - } - if blk >= targetBlock { - close(done) - return nil - } - } - } - }) - eg.Go(func() error { - for { - select { - case <-done: - return nil - case <-waitCtx.Done(): - return fmt.Errorf("timeout while generating txs waiting for block %d", targetBlock) - default: - if iErr := counter.Increment(); iErr != nil { - return iErr - } - } - } - }) - require.NoError(t, eg.Wait(), "failed while waiting for BHS window with tx-spam enabled on chainID=1337") -} diff --git a/go.md b/go.md index 1fff207c3f9..de581d56cd2 100644 --- a/go.md +++ b/go.md @@ -432,14 +432,11 @@ flowchart LR click chainlink-ton/deployment href "https://github.com/smartcontractkit/chainlink-ton" chainlink-tron/relayer --> chainlink-common click chainlink-tron/relayer href "https://github.com/smartcontractkit/chainlink-tron" - chainlink/core/scripts --> chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/cron-based - chainlink/core/scripts --> chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based + chainlink/core/scripts --> chainlink/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based chainlink/core/scripts --> chainlink/system-tests/lib click chainlink/core/scripts href "https://github.com/smartcontractkit/chainlink" chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/cron-based --> chainlink-common click chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/cron-based href "https://github.com/smartcontractkit/chainlink" - chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based --> chainlink/v2 - click chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based href "https://github.com/smartcontractkit/chainlink" chainlink/core/scripts/cre/environment/examples/workflows/v2/cron --> cre-sdk-go/capabilities/scheduler/cron click chainlink/core/scripts/cre/environment/examples/workflows/v2/cron href "https://github.com/smartcontractkit/chainlink" chainlink/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based --> chainlink-common @@ -486,6 +483,7 @@ flowchart LR chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evmread chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/httpaction chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/solana/solwrite + chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/vaultsecret click chainlink/system-tests/tests href "https://github.com/smartcontractkit/chainlink" chainlink/system-tests/tests/regression/cre/consensus --> cre-sdk-go/capabilities/scheduler/cron click chainlink/system-tests/tests/regression/cre/consensus href "https://github.com/smartcontractkit/chainlink" @@ -517,6 +515,8 @@ flowchart LR chainlink/system-tests/tests/smoke/cre/solana/solwrite --> cre-sdk-go/capabilities/blockchain/solana chainlink/system-tests/tests/smoke/cre/solana/solwrite --> cre-sdk-go/capabilities/scheduler/cron click chainlink/system-tests/tests/smoke/cre/solana/solwrite href "https://github.com/smartcontractkit/chainlink" + chainlink/system-tests/tests/smoke/cre/vaultsecret --> cre-sdk-go/capabilities/scheduler/cron + click chainlink/system-tests/tests/smoke/cre/vaultsecret href "https://github.com/smartcontractkit/chainlink" chainlink/v2 --> chainlink-automation chainlink/v2 --> chainlink-ccv chainlink/v2 --> chainlink-evm/contracts/cre/gobindings @@ -569,7 +569,6 @@ flowchart LR subgraph chainlink-repo[chainlink] chainlink/core/scripts chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/cron-based - chainlink/core/scripts/cre/environment/examples/workflows/v1/proof-of-reserve/web-trigger-based chainlink/core/scripts/cre/environment/examples/workflows/v2/cron chainlink/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based chainlink/deployment @@ -590,6 +589,7 @@ flowchart LR chainlink/system-tests/tests/smoke/cre/evmread chainlink/system-tests/tests/smoke/cre/httpaction chainlink/system-tests/tests/smoke/cre/solana/solwrite + chainlink/system-tests/tests/smoke/cre/vaultsecret chainlink/v2 end click chainlink-repo href "https://github.com/smartcontractkit/chainlink" diff --git a/go.mod b/go.mod index 04e009544df..04295d568a4 100644 --- a/go.mod +++ b/go.mod @@ -85,7 +85,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260224214816-cb23ec38649f github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 - github.com/smartcontractkit/chainlink-common v0.11.0 + github.com/smartcontractkit/chainlink-common v0.11.1 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 github.com/smartcontractkit/chainlink-data-streams v0.1.13 @@ -97,13 +97,14 @@ require ( github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260317132927-e8bc2c7b01f1 github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942 github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706 github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f - github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 + github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 github.com/smartcontractkit/chainlink-ton v0.0.0-20260318210736-c3f360fd19a8 github.com/smartcontractkit/cre-sdk-go v1.5.0 @@ -364,7 +365,6 @@ require ( github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 // indirect github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260218133534-cbd44da2856b // indirect diff --git a/go.sum b/go.sum index 7ab5e76ab0f..953646897a5 100644 --- a/go.sum +++ b/go.sum @@ -1235,8 +1235,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.0 h1:b/6fGMruUCKqxxzNBmTjCupRkd+m6LqvPCBBMTkpxU0= -github.com/smartcontractkit/chainlink-common v0.11.0/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.11.1 h1:JVTnqoQjdLDmQQXNgssmzEQnJK0gQ/0427LqS4UDuqE= +github.com/smartcontractkit/chainlink-common v0.11.1/go.mod h1:9W8E7tfchAsrSNHdMM1mzLmle+bL1P8Ou0I4LG1qNxw= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1271,12 +1271,12 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0. github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:ATjAPIVJibHRcIfiG47rEQkUIOoYa6KDvWj3zwCAw6g= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d h1:AJy55QJ/pBhXkZjc7N+ATnWfxrcjq9BI9DmdtdjwDUQ= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 h1:03tbcwjyIEjvHba1IWOj1sfThwebm2XNzyFHSuZtlWc= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 h1:fkS5FJpSozwxL2FA6OJDi7az2DrtMNiK1X5DWuHDyfA= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 h1:hhevsu8k7tlDRrYZmgAh7V4avGQDMvus1bwIlial3Ps= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 h1:oli+2uLU6jcrJGCuYFqk3475hiwL17SWlITWLv+tx/w= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0/go.mod h1:m/A3lqD7ms/RsQ9BT5P2uceYY0QX5mIt4KQxT2G6qEo= github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706 h1:z3sQK3dyfl9Rbm8Inj8irwvX6yQihASp1UvMjrfz6/w= @@ -1289,8 +1289,8 @@ github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f h1:3+vQMwuWL6+OqNutFqo/+gkczJwcr+MBPqeSxcjfI1Y= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 h1:XLMJ6FDQoEiqDNZ4B1MV9Vi1lL8vOfo9SzgqkM8IiuA= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c h1:7MUil5RQBxxnmfwp2bc1N4jv/8FVLH0hAkJupnGNMCg= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1:4mGJySR1GAJAAFRwEo6YiSKM2zSHzYT5b/FSmrpNUGI= github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:U3XStbEnbx/+L22n1/8aOIdgcGVxtsZB7p59xJGngAs= github.com/smartcontractkit/chainlink-ton v0.0.0-20260318210736-c3f360fd19a8 h1:TAa3dDeNpkR263GdXiH1hKEBev1btXLxDPnhP6VfFDY= diff --git a/integration-tests/Makefile b/integration-tests/Makefile index 98b285f9176..50e35f484a6 100644 --- a/integration-tests/Makefile +++ b/integration-tests/Makefile @@ -130,6 +130,7 @@ build_docker_image: check_github_token docker build -f ../core/chainlink.Dockerfile \ --build-arg COMMIT_SHA=$(git rev-parse HEAD) \ --build-arg VERSION_TAG=$(git describe --always) \ + --build-arg CL_AUTO_DOCKER_TAG=$(tag) \ --build-arg CHAINLINK_USER=chainlink \ --secret id=GIT_AUTH_TOKEN,env=GITHUB_TOKEN \ -t $(image):$(tag) ../ @@ -169,4 +170,3 @@ push-ccip-test-image: aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin $(base-image-registry).dkr.ecr.us-west-2.amazonaws.com docker tag chainlink-tests:latest $(base-image-registry).dkr.ecr.us-west-2.amazonaws.com/chainlink-tests:latest docker push $(base-image-registry).dkr.ecr.us-west-2.amazonaws.com/chainlink-tests:latest - diff --git a/integration-tests/ccip-tests/Makefile b/integration-tests/ccip-tests/Makefile index a56a63b9fd1..d97138e209a 100644 --- a/integration-tests/ccip-tests/Makefile +++ b/integration-tests/ccip-tests/Makefile @@ -66,11 +66,11 @@ test_smoke_ccip_default: set_config # example usage: make build_ccip_image image=chainlink-ccip tag=latest .PHONY: build_ccip_image build_ccip_image: - docker build -f ../../core/chainlink.Dockerfile --build-arg COMMIT_SHA=$(git rev-parse HEAD) --build-arg VERSION_TAG=$(git describe --always) --build-arg CHAINLINK_USER=chainlink -t $(image):$(tag) ../../ + docker build -f ../../core/chainlink.Dockerfile --build-arg COMMIT_SHA=$(git rev-parse HEAD) --build-arg VERSION_TAG=$(git describe --always) --build-arg CL_AUTO_DOCKER_TAG=$(tag) --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 VERSION_TAG=$(git describe --always) --build-arg CHAINLINK_USER=chainlink -t $(image):$(tag) ../../ + docker build -f ../../core/chainlink.debug.Dockerfile --build-arg COMMIT_SHA=$(git rev-parse HEAD) --build-arg VERSION_TAG=$(git describe --always) --build-arg CL_AUTO_DOCKER_TAG=$(tag) --build-arg CHAINLINK_USER=chainlink -t $(image):$(tag) ../../ diff --git a/integration-tests/docker/README.md b/integration-tests/docker/README.md index 84e88754aed..0e408595522 100644 --- a/integration-tests/docker/README.md +++ b/integration-tests/docker/README.md @@ -29,7 +29,7 @@ To acquire test coverage data for end-to-end (E2E) tests on the Chainlink Node, First, build the Chainlink Node Docker image with the `GO_COVER_FLAG` argument set to `true`. This enables the coverage flag in the build process. Here’s how you can do it: ``` - docker buildx build --platform linux/arm64 . -t localhost/chainlink-local:develop -f ./core/chainlink.Dockerfile --build-arg GO_COVER_FLAG=true + docker buildx build --platform linux/arm64 . -t localhost/chainlink-local:develop -f ./core/chainlink.Dockerfile --build-arg GO_COVER_FLAG=true --build-arg CL_AUTO_DOCKER_TAG=develop ``` Make sure to replace localhost/chainlink-local:develop with the appropriate repository and tag. @@ -45,4 +45,3 @@ After the tests are complete, the coverage report will be generated in HTML form ``` log.go:43: 16:29:46.73 INF Chainlink node coverage html report saved filePath=~/Downloads/go-coverage/-chain-reader/coverage.html ``` - diff --git a/integration-tests/go.mod b/integration-tests/go.mod index cd2b1b13594..c428033f6ec 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -28,7 +28,6 @@ require ( github.com/ethereum/go-ethereum v1.17.1 github.com/gagliardetto/solana-go v1.13.0 github.com/go-resty/resty/v2 v2.17.2 - github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.11.1 @@ -48,7 +47,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 - github.com/smartcontractkit/chainlink-common v0.11.0 + github.com/smartcontractkit/chainlink-common v0.11.1 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260320152158-2191d797b5ce @@ -301,6 +300,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v72 v72.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-tpm v0.9.0 // indirect @@ -513,16 +513,16 @@ require ( github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706 // indirect github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 // indirect github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3 // indirect github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f // indirect - github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 // indirect + github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c // indirect github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.3 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260218133534-cbd44da2856b // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 4d39acd499a..366452a2efe 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1626,8 +1626,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7db87ae70/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.0 h1:b/6fGMruUCKqxxzNBmTjCupRkd+m6LqvPCBBMTkpxU0= -github.com/smartcontractkit/chainlink-common v0.11.0/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.11.1 h1:JVTnqoQjdLDmQQXNgssmzEQnJK0gQ/0427LqS4UDuqE= +github.com/smartcontractkit/chainlink-common v0.11.1/go.mod h1:9W8E7tfchAsrSNHdMM1mzLmle+bL1P8Ou0I4LG1qNxw= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1664,14 +1664,14 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0. github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:ATjAPIVJibHRcIfiG47rEQkUIOoYa6KDvWj3zwCAw6g= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d h1:AJy55QJ/pBhXkZjc7N+ATnWfxrcjq9BI9DmdtdjwDUQ= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 h1:03tbcwjyIEjvHba1IWOj1sfThwebm2XNzyFHSuZtlWc= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 h1:fkS5FJpSozwxL2FA6OJDi7az2DrtMNiK1X5DWuHDyfA= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 h1:hhevsu8k7tlDRrYZmgAh7V4avGQDMvus1bwIlial3Ps= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 h1:oli+2uLU6jcrJGCuYFqk3475hiwL17SWlITWLv+tx/w= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 h1:AEnxv4HM3WD1RbQkRiFyb9cJ6YKAcqBp1CpIcFdZfuo= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= @@ -1686,8 +1686,8 @@ github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f h1:3+vQMwuWL6+OqNutFqo/+gkczJwcr+MBPqeSxcjfI1Y= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 h1:XLMJ6FDQoEiqDNZ4B1MV9Vi1lL8vOfo9SzgqkM8IiuA= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c h1:7MUil5RQBxxnmfwp2bc1N4jv/8FVLH0hAkJupnGNMCg= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1:4mGJySR1GAJAAFRwEo6YiSKM2zSHzYT5b/FSmrpNUGI= github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:U3XStbEnbx/+L22n1/8aOIdgcGVxtsZB7p59xJGngAs= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0 h1:5NdsaclAfx+p8lZUZ3WIqMW3M9Cze1ZVPENOQhha1pk= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index 581ba1091ba..1aa7f78e03d 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -27,7 +27,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 - github.com/smartcontractkit/chainlink-common v0.11.0 + github.com/smartcontractkit/chainlink-common v0.11.1 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260320152158-2191d797b5ce github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.3 @@ -490,17 +490,17 @@ require ( github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 // indirect github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706 // indirect github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 // indirect github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3 // indirect github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f // indirect - github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 // indirect + github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c // indirect github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 // indirect github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0 // indirect github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.7 // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index c8e1256ece5..b8a141d0a10 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1596,8 +1596,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7db87ae70/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.0 h1:b/6fGMruUCKqxxzNBmTjCupRkd+m6LqvPCBBMTkpxU0= -github.com/smartcontractkit/chainlink-common v0.11.0/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.11.1 h1:JVTnqoQjdLDmQQXNgssmzEQnJK0gQ/0427LqS4UDuqE= +github.com/smartcontractkit/chainlink-common v0.11.1/go.mod h1:9W8E7tfchAsrSNHdMM1mzLmle+bL1P8Ou0I4LG1qNxw= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1634,14 +1634,14 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0. github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:ATjAPIVJibHRcIfiG47rEQkUIOoYa6KDvWj3zwCAw6g= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d h1:AJy55QJ/pBhXkZjc7N+ATnWfxrcjq9BI9DmdtdjwDUQ= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 h1:03tbcwjyIEjvHba1IWOj1sfThwebm2XNzyFHSuZtlWc= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 h1:fkS5FJpSozwxL2FA6OJDi7az2DrtMNiK1X5DWuHDyfA= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 h1:hhevsu8k7tlDRrYZmgAh7V4avGQDMvus1bwIlial3Ps= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 h1:oli+2uLU6jcrJGCuYFqk3475hiwL17SWlITWLv+tx/w= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 h1:AEnxv4HM3WD1RbQkRiFyb9cJ6YKAcqBp1CpIcFdZfuo= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= @@ -1656,8 +1656,8 @@ github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f h1:3+vQMwuWL6+OqNutFqo/+gkczJwcr+MBPqeSxcjfI1Y= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 h1:XLMJ6FDQoEiqDNZ4B1MV9Vi1lL8vOfo9SzgqkM8IiuA= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c h1:7MUil5RQBxxnmfwp2bc1N4jv/8FVLH0hAkJupnGNMCg= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1:4mGJySR1GAJAAFRwEo6YiSKM2zSHzYT5b/FSmrpNUGI= github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:U3XStbEnbx/+L22n1/8aOIdgcGVxtsZB7p59xJGngAs= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0 h1:5NdsaclAfx+p8lZUZ3WIqMW3M9Cze1ZVPENOQhha1pk= diff --git a/integration-tests/smoke/README.md b/integration-tests/smoke/README.md index c8123cab26c..0fada805584 100644 --- a/integration-tests/smoke/README.md +++ b/integration-tests/smoke/README.md @@ -1,74 +1,3 @@ -## Smoke tests (local environments) - -These products are using local `testcontainers-go` environments: -- VRFv1 -- VRFv2 - -### Usage -``` -go test -v -run ${TestName} -``` -### Re-using environments -Configuration is still WIP, but you can make your environment re-usable by providing JSON config. - -Create `test_env.json` in the same dir -``` -export TEST_ENV_CONFIG_PATH=test_env.json -``` - -Here is an example for 3 nodes cluster -``` -{ - "networks": [ - "epic" - ], - "mockserver": { - "container_name": "mockserver", - "external_adapters_mock_urls": [ - "/epico1" - ] - }, - "geth": { - "container_name": "geth" - }, - "nodes": [ - { - "container_name": "cl-node-0", - "db_container_name": "cl-db-0" - }, - { - "container_name": "cl-node-1", - "db_container_name": "cl-db-1" - }, - { - "container_name": "cl-node-2", - "db_container_name": "cl-db-2" - } - ] -} -``` - -### Running against Live Testnets -1. Prepare your `overrides.toml` file with selected network and CL image name and version and save anywhere inside `integration-tests` folder. -```toml -[ChainlinkImage] -image="your-image" -version="your-version" - -[Network] -selected_networks=["polygon_mumbai"] - -[Network.RpcHttpUrls] -polygon_mumbai=["https://http.endpoint.com"] - -[Network.RpcWsUrls] -polygon_mumbai=["wss://ws.endpoint.com"] - -[Network.WalletKeys] -polygon_mumbai=["my_so_private_key"] -``` -Then execute: -```bash -go test -v -run ${TestName} -``` +## Smoke & load tests +All tests were moved to [devenv](../../devenv/). \ No newline at end of file diff --git a/integration-tests/smoke/vrfv2_test.go b/integration-tests/smoke/vrfv2_test.go deleted file mode 100644 index 7d1ef9caea7..00000000000 --- a/integration-tests/smoke/vrfv2_test.go +++ /dev/null @@ -1,1463 +0,0 @@ -package smoke - -import ( - "fmt" - "math" - "math/big" - "os" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/onsi/gomega" - "github.com/stretchr/testify/require" - "go.uber.org/zap/zapcore" - - commonassets "github.com/smartcontractkit/chainlink-common/pkg/assets" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/blockhash_store" - "github.com/smartcontractkit/chainlink-testing-framework/lib/logging" - "github.com/smartcontractkit/chainlink-testing-framework/lib/networks" - "github.com/smartcontractkit/chainlink-testing-framework/lib/testreporters" - "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/conversions" - "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/ptr" - "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" - "github.com/smartcontractkit/chainlink-testing-framework/seth" - - "github.com/smartcontractkit/chainlink/deployment/environment/nodeclient" - "github.com/smartcontractkit/chainlink/integration-tests/actions" - vrfcommon "github.com/smartcontractkit/chainlink/integration-tests/actions/vrf/common" - "github.com/smartcontractkit/chainlink/integration-tests/actions/vrf/vrfv2" - "github.com/smartcontractkit/chainlink/integration-tests/contracts" - "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" - tc "github.com/smartcontractkit/chainlink/integration-tests/testconfig" -) - -const ( - SethRootKeyIndex = 0 -) - -// vrfv2CleanUpFn is a cleanup function that captures pointers from context, in which it's called and uses them to clean up the test environment -var vrfv2CleanUpFn = func( - t **testing.T, - sethClient **seth.Client, - config **tc.TestConfig, - testEnv **test_env.CLClusterTestEnv, - vrfContracts **vrfcommon.VRFContracts, - subIDsForCancellingAfterTest *[]uint64, - walletAddress **string, -) func() { - return func() { - logger := logging.GetTestLogger(*t) - testConfig := **config - network := networks.MustGetSelectedNetworkConfig(testConfig.GetNetworkConfig())[0] - if network.Simulated { - logger.Info(). - Str("Network Name", network.Name). - Msg("Network is a simulated network. Skipping fund return for Coordinator Subscriptions.") - } else { - if *vrfContracts != nil && *sethClient != nil { - if *testConfig.VRFv2.General.CancelSubsAfterTestRun { - client := *sethClient - var returnToAddress string - if walletAddress == nil || *walletAddress == nil { - returnToAddress = client.MustGetRootKeyAddress().Hex() - } else { - returnToAddress = **walletAddress - } - // cancel subs and return funds to sub owner - vrfv2.CancelSubsAndReturnFunds(testcontext.Get(*t), *vrfContracts, returnToAddress, *subIDsForCancellingAfterTest, logger) - } - } else { - logger.Error().Msg("VRF Contracts and/or Seth client are nil. Cannot execute cleanup") - } - } - if !*testConfig.VRFv2.General.UseExistingEnv { - if *testEnv == nil { - logger.Error().Msg("Test environment is nil. Cannot execute cleanup") - return - } - if err := (*testEnv).Cleanup(test_env.CleanupOpts{TestName: (*t).Name()}); err != nil { - logger.Error().Err(err).Msg("Error cleaning up test environment") - } - } - } -} - -func TestVRFv2Basic(t *testing.T) { - t.Parallel() - var ( - testEnv *test_env.CLClusterTestEnv - vrfContracts *vrfcommon.VRFContracts - subIDsForCancellingAfterTest []uint64 - vrfKey *vrfcommon.VRFKeyData - nodeTypeToNodeMap map[vrfcommon.VRFNodeType]*vrfcommon.VRFNode - sethClient *seth.Client - ) - l := logging.GetTestLogger(t) - - config, err := tc.GetChainAndTestTypeSpecificConfig("Smoke", tc.VRFv2) - require.NoError(t, err, "Error getting config") - chainID := networks.MustGetSelectedNetworkConfig(config.GetNetworkConfig())[0].ChainID - - configPtr := &config - vrfEnvConfig := vrfcommon.VRFEnvConfig{ - TestConfig: config, - ChainID: chainID, - CleanupFn: vrfv2CleanUpFn(&t, &sethClient, &configPtr, &testEnv, &vrfContracts, &subIDsForCancellingAfterTest, nil), - } - newEnvConfig := vrfcommon.NewEnvConfig{ - NodesToCreate: []vrfcommon.VRFNodeType{vrfcommon.VRF}, - NumberOfTxKeysToCreate: 0, - UseVRFOwner: false, - UseTestCoordinator: false, - ChainlinkNodeLogScannerSettings: test_env.DefaultChainlinkNodeLogScannerSettings, - } - testEnv, vrfContracts, vrfKey, nodeTypeToNodeMap, sethClient, err = vrfv2.SetupVRFV2Universe(testcontext.Get(t), t, vrfEnvConfig, newEnvConfig, l) - require.NoError(t, err, "Error setting up VRFV2 universe") - - t.Run("Request Randomness", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - consumers, subIDsForRequestRandomness, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subIDForRequestRandomness := subIDsForRequestRandomness[0] - subscription, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForRequestRandomness) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscription, strconv.FormatUint(subIDForRequestRandomness, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDsForRequestRandomness...) - - subBalanceBeforeRequest := subscription.Balance - - // test and assert - _, randomWordsFulfilledEvent, err := vrfv2.RequestRandomnessAndWaitForFulfillment( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subIDForRequestRandomness, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - configCopy.VRFv2.General.RandomWordsFulfilledEventTimeout.Duration, - 0, - ) - require.NoError(t, err, "error requesting randomness and waiting for fulfilment") - - expectedSubBalanceJuels := new(big.Int).Sub(subBalanceBeforeRequest, randomWordsFulfilledEvent.Payment) - subscription, err = vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForRequestRandomness) - require.NoError(t, err, "error getting subscription information") - subBalanceAfterRequest := subscription.Balance - require.Equal(t, expectedSubBalanceJuels, subBalanceAfterRequest) - - status, err := consumers[0].GetRequestStatus(testcontext.Get(t), randomWordsFulfilledEvent.RequestId) - require.NoError(t, err, "error getting rand request status") - require.True(t, status.Fulfilled) - l.Info().Bool("Fulfilment Status", status.Fulfilled).Msg("Random Words Request Fulfilment Status") - - require.Len(t, status.RandomWords, int(*configCopy.VRFv2.General.NumberOfWords)) - for _, w := range status.RandomWords { - l.Info().Str("Output", w.String()).Msg("Randomness fulfilled") - require.Equal(t, 1, w.Cmp(big.NewInt(0)), "Expected the VRF job give an answer bigger than 0") - } - }) - t.Run("VRF Node waits block confirmation number specified by the consumer before sending fulfilment on-chain", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - testConfig := configCopy.VRFv2.General - - consumers, subIDs, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subID := subIDs[0] - subscription, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subID) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscription, strconv.FormatUint(subID, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDs...) - - expectedBlockNumberWait := uint16(10) - testConfig.MinimumConfirmations = ptr.Ptr[uint16](expectedBlockNumberWait) - randomWordsRequestedEvent, randomWordsFulfilledEvent, err := vrfv2.RequestRandomnessAndWaitForFulfillment( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subID, - vrfKey, - *testConfig.MinimumConfirmations, - *testConfig.CallbackGasLimit, - *testConfig.NumberOfWords, - *testConfig.RandomnessRequestCountPerRequest, - *testConfig.RandomnessRequestCountPerRequestDeviation, - testConfig.RandomWordsFulfilledEventTimeout.Duration, - 0, - ) - require.NoError(t, err, "error requesting randomness and waiting for fulfilment") - - // check that VRF node waited at least the number of blocks specified by the consumer in the rand request min confs field - blockNumberWait := randomWordsRequestedEvent.Raw.BlockNumber - randomWordsFulfilledEvent.Raw.BlockNumber - require.GreaterOrEqual(t, blockNumberWait, uint64(expectedBlockNumberWait)) - - status, err := consumers[0].GetRequestStatus(testcontext.Get(t), randomWordsFulfilledEvent.RequestId) - require.NoError(t, err, "error getting rand request status") - require.True(t, status.Fulfilled) - l.Info().Bool("Fulfilment Status", status.Fulfilled).Msg("Random Words Request Fulfilment Status") - }) - t.Run("CL Node VRF Job Runs", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - consumers, subIDsForJobRuns, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - - subIDForJobRuns := subIDsForJobRuns[0] - subscription, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForJobRuns) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscription, strconv.FormatUint(subIDForJobRuns, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDsForJobRuns...) - - jobRunsBeforeTest, err := nodeTypeToNodeMap[vrfcommon.VRF].CLNode.API.MustReadRunsByJob(nodeTypeToNodeMap[vrfcommon.VRF].Job.Data.ID) - require.NoError(t, err, "error reading job runs") - - // test and assert - _, _, err = vrfv2.RequestRandomnessAndWaitForFulfillment( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subIDForJobRuns, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - configCopy.VRFv2.General.RandomWordsFulfilledEventTimeout.Duration, - 0, - ) - require.NoError(t, err, "error requesting randomness and waiting for fulfilment") - - jobRuns, err := nodeTypeToNodeMap[vrfcommon.VRF].CLNode.API.MustReadRunsByJob(nodeTypeToNodeMap[vrfcommon.VRF].Job.Data.ID) - require.NoError(t, err, "error reading job runs") - require.Len(t, jobRuns.Data, len(jobRunsBeforeTest.Data)+1) - }) - t.Run("Direct Funding", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - wrapperContracts, wrapperSubID, err := vrfv2.SetupVRFV2WrapperEnvironment( - testcontext.Get(t), - sethClient, - &configCopy, - vrfContracts.LinkToken, - vrfContracts.MockETHLINKFeed, - vrfContracts.CoordinatorV2, - vrfKey.KeyHash, - 1, - ) - require.NoError(t, err) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, *wrapperSubID) - - wrapperConsumer := wrapperContracts.LoadTestConsumers[0] - - wrapperConsumerJuelsBalanceBeforeRequest, err := vrfContracts.LinkToken.BalanceOf(testcontext.Get(t), wrapperConsumer.Address()) - require.NoError(t, err, "Error getting wrapper consumer balance") - - wrapperSubscription, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), *wrapperSubID) - require.NoError(t, err, "Error getting subscription information") - subBalanceBeforeRequest := wrapperSubscription.Balance - - // Request Randomness and wait for fulfillment event - randomWordsFulfilledEvent, err := vrfv2.DirectFundingRequestRandomnessAndWaitForFulfillment( - l, - wrapperConsumer, - vrfContracts.CoordinatorV2, - *wrapperSubID, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - configCopy.VRFv2.General.RandomWordsFulfilledEventTimeout.Duration, - ) - require.NoError(t, err, "Error requesting randomness and waiting for fulfilment") - - // Check wrapper subscription balance - expectedSubBalanceJuels := new(big.Int).Sub(subBalanceBeforeRequest, randomWordsFulfilledEvent.Payment) - wrapperSubscription, err = vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), *wrapperSubID) - require.NoError(t, err, "Error getting subscription information") - subBalanceAfterRequest := wrapperSubscription.Balance - require.Equal(t, expectedSubBalanceJuels, subBalanceAfterRequest) - - // Check status of randomness request within the wrapper consumer contract - consumerStatus, err := wrapperConsumer.GetRequestStatus(testcontext.Get(t), randomWordsFulfilledEvent.RequestId) - require.NoError(t, err, "Error getting randomness request status") - require.True(t, consumerStatus.Fulfilled) - - // Check wrapper consumer LINK balance - expectedWrapperConsumerJuelsBalance := new(big.Int).Sub(wrapperConsumerJuelsBalanceBeforeRequest, consumerStatus.Paid) - wrapperConsumerJuelsBalanceAfterRequest, err := vrfContracts.LinkToken.BalanceOf(testcontext.Get(t), wrapperConsumer.Address()) - require.NoError(t, err, "Error getting wrapper consumer balance") - require.Equal(t, expectedWrapperConsumerJuelsBalance, wrapperConsumerJuelsBalanceAfterRequest) - - // Check random word count - require.Len(t, consumerStatus.RandomWords, int(*configCopy.VRFv2.General.NumberOfWords)) - for _, w := range consumerStatus.RandomWords { - l.Info().Str("Output", w.String()).Msg("Randomness fulfilled") - require.Equal(t, 1, w.Cmp(big.NewInt(0)), "Expected the VRF job give an answer bigger than 0") - } - - l.Info(). - Str("Consumer Balance Before Request (Link)", (*commonassets.Link)(wrapperConsumerJuelsBalanceBeforeRequest).Link()). - Str("Consumer Balance After Request (Link)", (*commonassets.Link)(wrapperConsumerJuelsBalanceAfterRequest).Link()). - Bool("Fulfilment Status", consumerStatus.Fulfilled). - Str("Paid by Consumer Contract (Link)", (*commonassets.Link)(consumerStatus.Paid).Link()). - Str("Paid by Coordinator Sub (Link)", (*commonassets.Link)(randomWordsFulfilledEvent.Payment).Link()). - Str("RequestTimestamp", consumerStatus.RequestTimestamp.String()). - Str("FulfilmentTimestamp", consumerStatus.FulfilmentTimestamp.String()). - Str("RequestBlockNumber", consumerStatus.RequestBlockNumber.String()). - Str("FulfilmentBlockNumber", consumerStatus.FulfilmentBlockNumber.String()). - Str("TX Hash", randomWordsFulfilledEvent.Raw.TxHash.String()). - Msg("Random Words Fulfilment Details For Link Billing") - }) - t.Run("Oracle Withdraw", func(t *testing.T) { - tests.SkipFlakey(t, "https://smartcontract-it.atlassian.net/browse/DX-527") - - configCopy := config.MustCopy().(tc.TestConfig) - consumers, subIDsForOracleWithDraw, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - - subIDForOracleWithdraw := subIDsForOracleWithDraw[0] - subscription, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForOracleWithdraw) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscription, strconv.FormatUint(subIDForOracleWithdraw, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDsForOracleWithDraw...) - - _, fulfilledEventLink, err := vrfv2.RequestRandomnessAndWaitForFulfillment( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subIDForOracleWithdraw, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - configCopy.VRFv2.General.RandomWordsFulfilledEventTimeout.Duration, - 0, - ) - require.NoError(t, err) - - amountToWithdrawLink := fulfilledEventLink.Payment - - defaultWalletBalanceLinkBeforeOracleWithdraw, err := vrfContracts.LinkToken.BalanceOf(testcontext.Get(t), sethClient.MustGetRootKeyAddress().Hex()) - require.NoError(t, err) - - l.Info(). - Str("Returning to", sethClient.MustGetRootKeyAddress().Hex()). - Str("Amount", amountToWithdrawLink.String()). - Msg("Invoking Oracle Withdraw for LINK") - - err = vrfContracts.CoordinatorV2.OracleWithdraw(sethClient.MustGetRootKeyAddress(), amountToWithdrawLink) - require.NoError(t, err, "Error withdrawing LINK from coordinator to default wallet") - - defaultWalletBalanceLinkAfterOracleWithdraw, err := vrfContracts.LinkToken.BalanceOf(testcontext.Get(t), sethClient.MustGetRootKeyAddress().Hex()) - require.NoError(t, err) - - require.Equal( - t, - 1, - defaultWalletBalanceLinkAfterOracleWithdraw.Cmp(defaultWalletBalanceLinkBeforeOracleWithdraw), - "LINK funds were not returned after oracle withdraw", - ) - }) - t.Run("Canceling Sub And Returning Funds", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - _, subIDsForCancelling, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subIDForCancelling := subIDsForCancelling[0] - subscription, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForCancelling) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscription, strconv.FormatUint(subIDForCancelling, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDsForCancelling...) - - testWalletAddress, err := actions.GenerateWallet() - require.NoError(t, err) - - testWalletBalanceLinkBeforeSubCancelling, err := vrfContracts.LinkToken.BalanceOf(testcontext.Get(t), testWalletAddress.String()) - require.NoError(t, err) - - subscriptionForCancelling, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForCancelling) - require.NoError(t, err, "error getting subscription information") - - subBalanceLink := subscriptionForCancelling.Balance - - l.Info(). - Str("Subscription Amount Link", subBalanceLink.String()). - Uint64("Returning funds from SubID", subIDForCancelling). - Str("Returning funds to", testWalletAddress.String()). - Msg("Canceling subscription and returning funds to subscription owner") - - cancellationTx, cancellationEvent, err := vrfContracts.CoordinatorV2.CancelSubscription(subIDForCancelling, testWalletAddress) - require.NoError(t, err, "Error canceling subscription") - - txGasUsed := new(big.Int).SetUint64(cancellationTx.Receipt.GasUsed) - // we don't have that information for older Geth versions - if cancellationTx.Receipt.EffectiveGasPrice == nil { - cancellationTx.Receipt.EffectiveGasPrice = new(big.Int).SetUint64(0) - } - cancellationTxFeeWei := new(big.Int).Mul(txGasUsed, cancellationTx.Receipt.EffectiveGasPrice) - - l.Info(). - Str("Cancellation Tx Fee Wei", cancellationTxFeeWei.String()). - Str("Effective Gas Price", cancellationTx.Receipt.EffectiveGasPrice.String()). - Uint64("Gas Used", cancellationTx.Receipt.GasUsed). - Msg("Cancellation TX Receipt") - - l.Info(). - Str("Returned Subscription Amount Link", cancellationEvent.Amount.String()). - Uint64("SubID", cancellationEvent.SubId). - Str("Returned to", cancellationEvent.To.String()). - Msg("Subscription Canceled Event") - - require.Equal(t, subBalanceLink, cancellationEvent.Amount, "SubscriptionCanceled event LINK amount is not equal to sub amount while canceling subscription") - - testWalletBalanceLinkAfterSubCancelling, err := vrfContracts.LinkToken.BalanceOf(testcontext.Get(t), testWalletAddress.String()) - require.NoError(t, err) - - // Verify that sub was deleted from Coordinator - _, err = vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForCancelling) - require.Error(t, err, "error not occurred when trying to get deleted subscription from old Coordinator after sub migration") - - subFundsReturnedLinkActual := new(big.Int).Sub(testWalletBalanceLinkAfterSubCancelling, testWalletBalanceLinkBeforeSubCancelling) - - l.Info(). - Str("Cancellation Tx Fee Wei", cancellationTxFeeWei.String()). - Str("Sub Funds Returned Actual - Link", subFundsReturnedLinkActual.String()). - Str("Sub Balance - Link", subBalanceLink.String()). - Msg("Sub funds returned") - - require.Equal(t, 0, subBalanceLink.Cmp(subFundsReturnedLinkActual), "Returned LINK funds are not equal to sub balance that was cancelled") - }) - t.Run("Owner Canceling Sub And Returning Funds While Having Pending Requests", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - // Underfund subscription to force fulfillments to fail - configCopy.VRFv2.General.SubscriptionFundingAmountLink = ptr.Ptr(float64(0)) - - consumers, subIDsForOwnerCancelling, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subIDForOwnerCancelling := subIDsForOwnerCancelling[0] - subscriptionForCancelling, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForOwnerCancelling) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscriptionForCancelling, strconv.FormatUint(subIDForOwnerCancelling, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDsForOwnerCancelling...) - - // No GetActiveSubscriptionIds function available - skipping check - - pendingRequestsExist, err := vrfContracts.CoordinatorV2.PendingRequestsExist(testcontext.Get(t), subIDForOwnerCancelling) - require.NoError(t, err) - require.False(t, pendingRequestsExist, "Pending requests should not exist") - - // Request randomness - should fail due to underfunded subscription - randomWordsFulfilledEventTimeout := 5 * time.Second - _, _, err = vrfv2.RequestRandomnessAndWaitForFulfillment( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subIDForOwnerCancelling, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - randomWordsFulfilledEventTimeout, - 0, - ) - require.Error(t, err, "Error should occur while waiting for fulfilment due to low sub balance") - - pendingRequestsExist, err = vrfContracts.CoordinatorV2.PendingRequestsExist(testcontext.Get(t), subIDForOwnerCancelling) - require.NoError(t, err) - require.True(t, pendingRequestsExist, "Pending requests should exist after unfilfulled requests due to low sub balance") - - walletBalanceLinkBeforeSubCancelling, err := vrfContracts.LinkToken.BalanceOf(testcontext.Get(t), sethClient.MustGetRootKeyAddress().Hex()) - require.NoError(t, err) - - subscriptionForCancelling, err = vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForOwnerCancelling) - require.NoError(t, err, "Error getting subscription information") - subBalanceLink := subscriptionForCancelling.Balance - - l.Info(). - Str("Subscription Amount Link", subBalanceLink.String()). - Uint64("Returning funds from SubID", subIDForOwnerCancelling). - Str("Returning funds to", sethClient.MustGetRootKeyAddress().Hex()). - Msg("Canceling subscription and returning funds to subscription owner") - - // Call OwnerCancelSubscription - cancellationTx, cancellationEvent, err := vrfContracts.CoordinatorV2.OwnerCancelSubscription(subIDForOwnerCancelling) - require.NoError(t, err, "Error canceling subscription") - - txGasUsed := new(big.Int).SetUint64(cancellationTx.Receipt.GasUsed) - // we don't have that information for older Geth versions - if cancellationTx.Receipt.EffectiveGasPrice == nil { - cancellationTx.Receipt.EffectiveGasPrice = new(big.Int).SetUint64(0) - } - cancellationTxFeeWei := new(big.Int).Mul(txGasUsed, cancellationTx.Receipt.EffectiveGasPrice) - - l.Info(). - Str("Cancellation Tx Fee Wei", cancellationTxFeeWei.String()). - Str("Effective Gas Price", cancellationTx.Receipt.EffectiveGasPrice.String()). - Uint64("Gas Used", cancellationTx.Receipt.GasUsed). - Msg("Cancellation TX Receipt") - - l.Info(). - Str("Returned Subscription Amount Link", cancellationEvent.Amount.String()). - Uint64("SubID", cancellationEvent.SubId). - Str("Returned to", cancellationEvent.To.String()). - Msg("Subscription Canceled Event") - - require.Equal(t, subBalanceLink, cancellationEvent.Amount, "SubscriptionCanceled event LINK amount is not equal to sub amount while canceling subscription") - - walletBalanceLinkAfterSubCancelling, err := vrfContracts.LinkToken.BalanceOf(testcontext.Get(t), sethClient.MustGetRootKeyAddress().Hex()) - require.NoError(t, err) - - // Verify that subscription was deleted from Coordinator contract - _, err = vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForOwnerCancelling) - l.Info(). - Str("Expected error message", err.Error()) - require.Error(t, err, "Error did not occur when fetching deleted subscription from the Coordinator after owner cancelation") - - subFundsReturnedLinkActual := new(big.Int).Sub(walletBalanceLinkAfterSubCancelling, walletBalanceLinkBeforeSubCancelling) - l.Info(). - Str("Wallet Balance Before Owner Cancelation", walletBalanceLinkBeforeSubCancelling.String()). - Str("Cancellation Tx Fee Wei", cancellationTxFeeWei.String()). - Str("Sub Funds Returned Actual - Link", subFundsReturnedLinkActual.String()). - Str("Sub Balance - Link", subBalanceLink.String()). - Str("Wallet Balance After Owner Cancelation", walletBalanceLinkAfterSubCancelling.String()). - Msg("Sub funds returned") - - require.Equal(t, 0, subBalanceLink.Cmp(subFundsReturnedLinkActual), "Returned LINK funds are not equal to sub balance that was cancelled") - - // Again, there is no GetActiveSubscriptionIds method on the v2 Coordinator contract, so we can't double check that the cancelled - // subID is no longer in the list of active subs - }) -} - -func TestVRFv2MultipleSendingKeys(t *testing.T) { - t.Parallel() - var ( - testEnv *test_env.CLClusterTestEnv - vrfContracts *vrfcommon.VRFContracts - subIDsForCancellingAfterTest []uint64 - vrfKey *vrfcommon.VRFKeyData - nodeTypeToNodeMap map[vrfcommon.VRFNodeType]*vrfcommon.VRFNode - sethClient *seth.Client - ) - l := logging.GetTestLogger(t) - - config, err := tc.GetChainAndTestTypeSpecificConfig("Smoke", tc.VRFv2) - if err != nil { - t.Fatal(err) - } - chainID := networks.MustGetSelectedNetworkConfig(config.GetNetworkConfig())[0].ChainID - - configPtr := &config - vrfEnvConfig := vrfcommon.VRFEnvConfig{ - TestConfig: config, - ChainID: chainID, - CleanupFn: vrfv2CleanUpFn(&t, &sethClient, &configPtr, &testEnv, &vrfContracts, &subIDsForCancellingAfterTest, nil), - } - newEnvConfig := vrfcommon.NewEnvConfig{ - NodesToCreate: []vrfcommon.VRFNodeType{vrfcommon.VRF}, - NumberOfTxKeysToCreate: 2, - UseVRFOwner: false, - UseTestCoordinator: false, - ChainlinkNodeLogScannerSettings: test_env.DefaultChainlinkNodeLogScannerSettings, - } - testEnv, vrfContracts, vrfKey, nodeTypeToNodeMap, sethClient, err = vrfv2.SetupVRFV2Universe(testcontext.Get(t), t, vrfEnvConfig, newEnvConfig, l) - require.NoError(t, err, "Error setting up VRFV2 universe") - - t.Run("Request Randomness with multiple sending keys", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - - consumers, subIDsForMultipleSendingKeys, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subIDForMultipleSendingKeys := subIDsForMultipleSendingKeys[0] - subscriptionForMultipleSendingKeys, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForMultipleSendingKeys) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscriptionForMultipleSendingKeys, strconv.FormatUint(subIDForMultipleSendingKeys, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDsForMultipleSendingKeys...) - - txKeys, _, err := nodeTypeToNodeMap[vrfcommon.VRF].CLNode.API.ReadTxKeys("evm") - require.NoError(t, err, "error reading tx keys") - - require.Len(t, txKeys.Data, newEnvConfig.NumberOfTxKeysToCreate+1) - - var fulfillmentTxFromAddresses []string - for i := 0; i < newEnvConfig.NumberOfTxKeysToCreate+1; i++ { - _, randomWordsFulfilledEvent, err := vrfv2.RequestRandomnessAndWaitForFulfillment( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subIDForMultipleSendingKeys, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - configCopy.VRFv2.General.RandomWordsFulfilledEventTimeout.Duration, - 0, - ) - require.NoError(t, err, "error requesting randomness and waiting for fulfilment") - fulfillmentTx, _, err := sethClient.Client.TransactionByHash(testcontext.Get(t), randomWordsFulfilledEvent.Raw.TxHash) - require.NoError(t, err, "error getting tx from hash") - fulfillmentTxFromAddress, err := actions.GetTxFromAddress(fulfillmentTx) - require.NoError(t, err, "error getting tx from address") - fulfillmentTxFromAddresses = append(fulfillmentTxFromAddresses, fulfillmentTxFromAddress) - } - require.Len(t, fulfillmentTxFromAddresses, newEnvConfig.NumberOfTxKeysToCreate+1) - var txKeyAddresses []string - for _, txKey := range txKeys.Data { - txKeyAddresses = append(txKeyAddresses, txKey.Attributes.Address) - } - less := func(a, b string) bool { return a < b } - equalIgnoreOrder := cmp.Diff(txKeyAddresses, fulfillmentTxFromAddresses, cmpopts.SortSlices(less)) == "" - require.True(t, equalIgnoreOrder) - }) -} - -func TestVRFOwner(t *testing.T) { - tests.SkipFlakey(t, "https://smartcontract-it.atlassian.net/browse/DX-565") - - t.Parallel() - var ( - testEnv *test_env.CLClusterTestEnv - vrfContracts *vrfcommon.VRFContracts - subIDsForCancellingAfterTest []uint64 - vrfKey *vrfcommon.VRFKeyData - sethClient *seth.Client - ) - l := logging.GetTestLogger(t) - - config, err := tc.GetChainAndTestTypeSpecificConfig("Smoke", tc.VRFv2) - require.NoError(t, err, "Error getting config") - chainID := networks.MustGetSelectedNetworkConfig(config.GetNetworkConfig())[0].ChainID - - configPtr := &config - vrfEnvConfig := vrfcommon.VRFEnvConfig{ - TestConfig: config, - ChainID: chainID, - CleanupFn: vrfv2CleanUpFn(&t, &sethClient, &configPtr, &testEnv, &vrfContracts, &subIDsForCancellingAfterTest, nil), - } - newEnvConfig := vrfcommon.NewEnvConfig{ - NodesToCreate: []vrfcommon.VRFNodeType{vrfcommon.VRF}, - NumberOfTxKeysToCreate: 0, - UseVRFOwner: true, - UseTestCoordinator: true, - ChainlinkNodeLogScannerSettings: test_env.DefaultChainlinkNodeLogScannerSettings, - } - testEnv, vrfContracts, vrfKey, _, sethClient, err = vrfv2.SetupVRFV2Universe(testcontext.Get(t), t, vrfEnvConfig, newEnvConfig, l) - require.NoError(t, err, "Error setting up VRFV2 universe") - - t.Run("Request Randomness With Force-Fulfill", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - - consumers, subIDsForForceFulfill, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subIDForForceFulfill := subIDsForForceFulfill[0] - subscriptionForMultipleSendingKeys, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForForceFulfill) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscriptionForMultipleSendingKeys, strconv.FormatUint(subIDForForceFulfill, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDsForForceFulfill...) - - vrfCoordinatorOwner, err := vrfContracts.CoordinatorV2.GetOwner(testcontext.Get(t)) - require.NoError(t, err) - require.Equal(t, vrfContracts.VRFOwner.Address(), vrfCoordinatorOwner.String()) - - err = vrfContracts.LinkToken.Transfer( - consumers[0].Address(), - conversions.EtherToWei(big.NewFloat(*configCopy.VRFv2.General.SubscriptionFundingAmountLink)), - ) - require.NoError(t, err, "error transferring link to consumer contract") - - consumerLinkBalance, err := vrfContracts.LinkToken.BalanceOf(testcontext.Get(t), consumers[0].Address()) - require.NoError(t, err, "error getting consumer link balance") - l.Info(). - Str("Balance", conversions.WeiToEther(consumerLinkBalance).String()). - Str("Consumer", consumers[0].Address()). - Msg("Consumer Link Balance") - - err = vrfContracts.MockETHLINKFeed.SetBlockTimestampDeduction(big.NewInt(3)) - require.NoError(t, err) - - // test and assert - _, randFulfilledEvent, _, err := vrfv2.RequestRandomnessWithForceFulfillAndWaitForFulfillment( - l, - consumers[0], - vrfContracts.CoordinatorV2, - vrfContracts.VRFOwner, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - conversions.EtherToWei(big.NewFloat(5)), - common.HexToAddress(vrfContracts.LinkToken.Address()), - time.Minute*2, - ) - require.NoError(t, err, "error requesting randomness with force-fulfillment and waiting for fulfilment") - require.Equal(t, 0, randFulfilledEvent.Payment.Cmp(big.NewInt(0)), "Forced Fulfilled Randomness's Payment should be 0") - - status, err := consumers[0].GetRequestStatus(testcontext.Get(t), randFulfilledEvent.RequestId) - require.NoError(t, err, "error getting rand request status") - require.True(t, status.Fulfilled) - l.Info().Bool("Fulfilment Status", status.Fulfilled).Msg("Random Words Request Fulfilment Status") - - require.Len(t, status.RandomWords, int(*configCopy.VRFv2.General.NumberOfWords)) - for _, w := range status.RandomWords { - l.Info().Str("Output", w.String()).Msg("Randomness fulfilled") - require.Equal(t, 1, w.Cmp(big.NewInt(0)), "Expected the VRF job give an answer bigger than 0") - } - - coordinatorConfig, err := vrfContracts.CoordinatorV2.GetConfig(testcontext.Get(t)) - require.NoError(t, err, "error getting coordinator config") - - coordinatorFeeConfig, err := vrfContracts.CoordinatorV2.GetFeeConfig(testcontext.Get(t)) - require.NoError(t, err, "error getting coordinator fee config") - - coordinatorFallbackWeiPerUnitLinkConfig, err := vrfContracts.CoordinatorV2.GetFallbackWeiPerUnitLink(testcontext.Get(t)) - require.NoError(t, err, "error getting coordinator FallbackWeiPerUnitLink") - - require.Equal(t, *configCopy.VRFv2.General.StalenessSeconds, coordinatorConfig.StalenessSeconds) - require.Equal(t, *configCopy.VRFv2.General.GasAfterPaymentCalculation, coordinatorConfig.GasAfterPaymentCalculation) - require.Equal(t, *configCopy.VRFv2.General.MinimumConfirmations, coordinatorConfig.MinimumRequestConfirmations) - require.Equal(t, *configCopy.VRFv2.General.FulfillmentFlatFeeLinkPPMTier1, coordinatorFeeConfig.FulfillmentFlatFeeLinkPPMTier1) - require.Equal(t, *configCopy.VRFv2.General.ReqsForTier2, coordinatorFeeConfig.ReqsForTier2.Int64()) - require.Equal(t, *configCopy.VRFv2.General.FallbackWeiPerUnitLink, coordinatorFallbackWeiPerUnitLinkConfig.String()) - }) -} - -func TestVRFV2WithBHS(t *testing.T) { - t.Parallel() - var ( - testEnv *test_env.CLClusterTestEnv - vrfContracts *vrfcommon.VRFContracts - subIDsForCancellingAfterTest []uint64 - vrfKey *vrfcommon.VRFKeyData - nodeTypeToNodeMap map[vrfcommon.VRFNodeType]*vrfcommon.VRFNode - sethClient *seth.Client - ) - l := logging.GetTestLogger(t) - - config, err := tc.GetChainAndTestTypeSpecificConfig("Smoke", tc.VRFv2) - require.NoError(t, err, "Error getting config") - vrfv2Config := config.VRFv2 - chainID := networks.MustGetSelectedNetworkConfig(config.GetNetworkConfig())[0].ChainID - configPtr := &config - - // decrease default span for checking blockhashes for unfulfilled requests - vrfv2Config.General.BHSJobWaitBlocks = ptr.Ptr(2) - vrfv2Config.General.BHSJobLookBackBlocks = ptr.Ptr(20) - vrfEnvConfig := vrfcommon.VRFEnvConfig{ - TestConfig: config, - ChainID: chainID, - CleanupFn: vrfv2CleanUpFn(&t, &sethClient, &configPtr, &testEnv, &vrfContracts, &subIDsForCancellingAfterTest, nil), - } - newEnvConfig := vrfcommon.NewEnvConfig{ - NodesToCreate: []vrfcommon.VRFNodeType{vrfcommon.VRF, vrfcommon.BHS}, - NumberOfTxKeysToCreate: 0, - UseVRFOwner: false, - UseTestCoordinator: false, - ChainlinkNodeLogScannerSettings: test_env.DefaultChainlinkNodeLogScannerSettings, - } - testEnv, vrfContracts, vrfKey, nodeTypeToNodeMap, sethClient, err = vrfv2.SetupVRFV2Universe(testcontext.Get(t), t, vrfEnvConfig, newEnvConfig, l) - require.NoError(t, err, "Error setting up VRFV2 universe") - - t.Run("BHS Job with complete E2E - wait 256 blocks to see if Rand Request is fulfilled", func(t *testing.T) { - if os.Getenv("TEST_UNSKIP") != "true" { - t.Skip("Skipped due to long execution time. Should be run on-demand on live testnet with TEST_UNSKIP=\"true\".") - } - // BHS node should fill in blockhashes into BHS contract depending on the waitBlocks and lookBackBlocks settings - configCopy := config.MustCopy().(tc.TestConfig) - // Underfund Subscription - configCopy.VRFv2.General.SubscriptionFundingAmountLink = ptr.Ptr(float64(0)) - consumers, subIDsForBHS, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subIDForBHS := subIDsForBHS[0] - subscriptionForBHS, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForBHS) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscriptionForBHS, strconv.FormatUint(subIDForBHS, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDsForBHS...) - - randomWordsRequestedEvent, err := vrfv2.RequestRandomness( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subIDForBHS, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - SethRootKeyIndex, - ) - require.NoError(t, err, "error requesting randomness") - - vrfcommon.LogRandomnessRequestedEvent(l, vrfContracts.CoordinatorV2, randomWordsRequestedEvent, false, 0) - randRequestBlockNumber := randomWordsRequestedEvent.Raw.BlockNumber - var wg sync.WaitGroup - wg.Add(1) - // Wait at least 256 blocks - _, err = actions.WaitForBlockNumberToBe( - testcontext.Get(t), - randRequestBlockNumber+uint64(257), - sethClient, - &wg, - nil, - configCopy.VRFv2.General.WaitFor256BlocksTimeout.Duration, - l, - ) - wg.Wait() - require.NoError(t, err) - err = vrfv2.FundSubscriptions(big.NewFloat(*configCopy.VRFv2.General.SubscriptionFundingAmountLink), vrfContracts.LinkToken, vrfContracts.CoordinatorV2, subIDsForBHS) - require.NoError(t, err, "error funding subscriptions") - - randomWordsFulfilledEvent, err := vrfv2.WaitRandomWordsFulfilledEvent( - vrfContracts.CoordinatorV2, - randomWordsRequestedEvent.RequestId, - randomWordsRequestedEvent.Raw.BlockNumber, - configCopy.VRFv2.General.RandomWordsFulfilledEventTimeout.Duration, - l, - ) - require.NoError(t, err, "error waiting for randomness fulfilled event") - vrfcommon.LogRandomWordsFulfilledEvent(l, vrfContracts.CoordinatorV2, randomWordsFulfilledEvent, false, 0) - status, err := consumers[0].GetRequestStatus(testcontext.Get(t), randomWordsFulfilledEvent.RequestId) - require.NoError(t, err, "error getting rand request status") - require.True(t, status.Fulfilled) - l.Info().Bool("Fulfilment Status", status.Fulfilled).Msg("Random Words Request Fulfilment Status") - }) - - t.Run("BHS Job should fill in blockhashes into BHS contract for unfulfilled requests", func(t *testing.T) { - // BHS node should fill in blockhashes into BHS contract depending on the waitBlocks and lookBackBlocks settings - configCopy := config.MustCopy().(tc.TestConfig) - // Underfund Subscription - configCopy.VRFv2.General.SubscriptionFundingAmountLink = ptr.Ptr(float64(0)) - - consumers, subIDsForBHS, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subIDForBHS := subIDsForBHS[0] - subscriptionForBHS, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subIDForBHS) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscriptionForBHS, strconv.FormatUint(subIDForBHS, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDsForBHS...) - - randomWordsRequestedEvent, err := vrfv2.RequestRandomness( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subIDForBHS, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - SethRootKeyIndex, - ) - require.NoError(t, err, "error requesting randomness") - randRequestBlockNumber := randomWordsRequestedEvent.Raw.BlockNumber - _, err = vrfContracts.BHS.GetBlockHash(testcontext.Get(t), new(big.Int).SetUint64(randRequestBlockNumber)) - require.Error(t, err, "error not occurred when getting blockhash for a blocknumber which was not stored in BHS contract") - - blocks := *configCopy.VRFv2.General.BHSJobWaitBlocks - if blocks < 0 { - t.Fatalf("negative blocks: %d", blocks) - } - var wg sync.WaitGroup - wg.Add(1) - _, err = actions.WaitForBlockNumberToBe( - testcontext.Get(t), - randRequestBlockNumber+uint64(blocks), - sethClient, - &wg, - nil, - time.Minute*1, - l, - ) - wg.Wait() - require.NoError(t, err, "error waiting for blocknumber to be") - - metrics, err := consumers[0].GetLoadTestMetrics(testcontext.Get(t)) - require.Equal(t, 0, metrics.RequestCount.Cmp(big.NewInt(1))) - require.Equal(t, 0, metrics.FulfilmentCount.Cmp(big.NewInt(0))) - gom := gomega.NewGomegaWithT(t) - - if !*configCopy.VRFv2.General.UseExistingEnv { - l.Info().Msg("Checking BHS Node's transactions") - var clNodeTxs *nodeclient.TransactionsData - var txHash string - gom.Eventually(func(g gomega.Gomega) { - clNodeTxs, _, err = nodeTypeToNodeMap[vrfcommon.BHS].CLNode.API.ReadTransactions() - g.Expect(err).ShouldNot(gomega.HaveOccurred(), "error getting CL Node transactions") - g.Expect(clNodeTxs.Data).Should(gomega.HaveLen(1), "Expected 1 tx posted by BHS Node, but found %d", len(clNodeTxs.Data)) - txHash = clNodeTxs.Data[0].Attributes.Hash - l.Info(). - Str("TX Hash", txHash). - Int("Number of TXs", len(clNodeTxs.Data)). - Msg("BHS Node txs") - }, "2m", "1s").Should(gomega.Succeed()) - - require.Equal(t, strings.ToLower(vrfContracts.BHS.Address()), strings.ToLower(clNodeTxs.Data[0].Attributes.To)) - - bhsStoreTx, _, err := sethClient.Client.TransactionByHash(testcontext.Get(t), common.HexToHash(txHash)) - require.NoError(t, err, "error getting tx from hash") - - bhsStoreTxInputData, err := actions.DecodeTxInputData(blockhash_store.BlockhashStoreABI, bhsStoreTx.Data()) - require.NoError(t, err, "error decoding tx input data") - l.Info(). - Str("Block Number", bhsStoreTxInputData["n"].(*big.Int).String()). - Msg("BHS Node's Store Blockhash for Blocknumber Method TX") - require.Equal(t, randRequestBlockNumber, bhsStoreTxInputData["n"].(*big.Int).Uint64()) - } else { - l.Warn().Msg("Skipping BHS Node's transactions check as existing env is used") - } - var randRequestBlockHash [32]byte - gom.Eventually(func(g gomega.Gomega) { - randRequestBlockHash, err = vrfContracts.BHS.GetBlockHash(testcontext.Get(t), new(big.Int).SetUint64(randRequestBlockNumber)) - g.Expect(err).ShouldNot(gomega.HaveOccurred(), "error getting blockhash for a blocknumber which was stored in BHS contract") - }, "2m", "1s").Should(gomega.Succeed()) - l.Info(). - Str("Randomness Request's Blockhash", randomWordsRequestedEvent.Raw.BlockHash.String()). - Str("Block Hash stored by BHS contract", fmt.Sprintf("0x%x", randRequestBlockHash)). - Msg("BHS Contract's stored Blockhash for Randomness Request") - require.Equal(t, 0, randomWordsRequestedEvent.Raw.BlockHash.Cmp(randRequestBlockHash)) - }) -} - -func TestVRFV2NodeReorg(t *testing.T) { - tests.SkipFlakey(t, "https://smartcontract-it.atlassian.net/browse/DEVSVCS-829") - t.Parallel() - var ( - env *test_env.CLClusterTestEnv - vrfContracts *vrfcommon.VRFContracts - subIDsForCancellingAfterTest []uint64 - vrfKey *vrfcommon.VRFKeyData - sethClient *seth.Client - ) - l := logging.GetTestLogger(t) - - config, err := tc.GetChainAndTestTypeSpecificConfig("Smoke", tc.VRFv2) - require.NoError(t, err, "Error getting config") - network := networks.MustGetSelectedNetworkConfig(config.GetNetworkConfig())[0] - if !network.Simulated { - t.Skip("Skipped since Reorg test could only be run on Simulated chain.") - } - chainID := network.ChainID - - configPtr := &config - chainlinkNodeLogScannerSettings := test_env.GetDefaultChainlinkNodeLogScannerSettingsWithExtraAllowedMessages( - testreporters.NewAllowedLogMessage( - "Got very old block.", - "Test is expecting a reorg to occur", - zapcore.DPanicLevel, - testreporters.WarnAboutAllowedMsgs_No), - testreporters.NewAllowedLogMessage( - "Reorg greater than finality depth detected", - "Test is expecting a reorg to occur", - zapcore.DPanicLevel, - testreporters.WarnAboutAllowedMsgs_No), - ) - vrfEnvConfig := vrfcommon.VRFEnvConfig{ - TestConfig: config, - ChainID: chainID, - CleanupFn: vrfv2CleanUpFn(&t, &sethClient, &configPtr, &env, &vrfContracts, &subIDsForCancellingAfterTest, nil), - } - newEnvConfig := vrfcommon.NewEnvConfig{ - NodesToCreate: []vrfcommon.VRFNodeType{vrfcommon.VRF}, - NumberOfTxKeysToCreate: 0, - UseVRFOwner: false, - UseTestCoordinator: false, - ChainlinkNodeLogScannerSettings: chainlinkNodeLogScannerSettings, - } - env, vrfContracts, vrfKey, _, sethClient, err = vrfv2.SetupVRFV2Universe(testcontext.Get(t), t, vrfEnvConfig, newEnvConfig, l) - require.NoError(t, err, "Error setting up VRFv2 universe") - - consumers, subIDs, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - config, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subID := subIDs[0] - subscription, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subID) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscription, strconv.FormatUint(subID, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDs...) - - t.Run("Reorg on fulfillment", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - configCopy.VRFv2.General.MinimumConfirmations = ptr.Ptr[uint16](10) - - // 1. request randomness and wait for fulfillment for blockhash from Reorged Fork - randomWordsRequestedEvent, randomWordsFulfilledEventOnReorgedFork, err := vrfv2.RequestRandomnessAndWaitForFulfillment( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subID, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - configCopy.VRFv2.General.RandomWordsFulfilledEventTimeout.Duration, - 0, - ) - require.NoError(t, err) - - // rewind chain to block number after the request was made, but before the request was fulfilled - rewindChainToBlock := randomWordsRequestedEvent.Raw.BlockNumber + 1 - - rpcUrl, err := vrfcommon.GetRPCUrl(env, chainID) - require.NoError(t, err, "error getting rpc url") - - // 2. rewind chain by n number of blocks - basically, mimicking reorg scenario - latestBlockNumberAfterReorg, err := vrfcommon.RewindSimulatedChainToBlockNumber(testcontext.Get(t), sethClient, rpcUrl, rewindChainToBlock, l) - require.NoError(t, err, "error rewinding chain to block number %d", rewindChainToBlock) - - // 3.1 ensure that chain is reorged and latest block number is greater than the block number when request was made - require.Greater(t, latestBlockNumberAfterReorg, randomWordsRequestedEvent.Raw.BlockNumber) - - // 3.2 ensure that chain is reorged and latest block number is less than the block number when fulfilment was performed - require.Less(t, latestBlockNumberAfterReorg, randomWordsFulfilledEventOnReorgedFork.Raw.BlockNumber) - - // 4. wait for the fulfillment which VRF Node will generate for Canonical chain - _, err = vrfv2.WaitRandomWordsFulfilledEvent( - vrfContracts.CoordinatorV2, - randomWordsRequestedEvent.RequestId, - randomWordsRequestedEvent.Raw.BlockNumber, - configCopy.VRFv2.General.RandomWordsFulfilledEventTimeout.Duration, - l, - ) - - require.NoError(t, err, "error waiting for randomness fulfilled event") - }) - - t.Run("Reorg on rand request", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - // 1. set minimum confirmations to higher value so that we can be sure that request won't be fulfilled before reorg - configCopy.VRFv2.General.MinimumConfirmations = ptr.Ptr[uint16](6) - - // 2. request randomness - randomWordsRequestedEvent, err := vrfv2.RequestRandomness( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subID, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - SethRootKeyIndex, - ) - require.NoError(t, err) - - // rewind chain to block number before the randomness request was made - rewindChainToBlockNumber := randomWordsRequestedEvent.Raw.BlockNumber - 3 - - rpcUrl, err := vrfcommon.GetRPCUrl(env, chainID) - require.NoError(t, err, "error getting rpc url") - - // 3. rewind chain by n number of blocks - basically, mimicking reorg scenario - latestBlockNumberAfterReorg, err := vrfcommon.RewindSimulatedChainToBlockNumber(testcontext.Get(t), sethClient, rpcUrl, rewindChainToBlockNumber, l) - require.NoError(t, err, "error rewinding chain to block number %d", rewindChainToBlockNumber) - - // 4. ensure that chain is reorged and latest block number is less than the block number when request was made - require.Less(t, latestBlockNumberAfterReorg, randomWordsRequestedEvent.Raw.BlockNumber) - - // 5. ensure that rand request is not fulfilled for the request which was made on reorged fork - // For context - when performing debug_setHead on geth simulated chain and therefore rewinding chain to a previous block, - // then tx that was mined after reorg will not appear in canonical chain contrary to real world scenario - // Hence, we only verify that VRF node will not generate fulfillment for the reorged fork request - _, err = vrfv2.WaitRandomWordsFulfilledEvent( - vrfContracts.CoordinatorV2, - randomWordsRequestedEvent.RequestId, - randomWordsRequestedEvent.Raw.BlockNumber, - time.Second*10, - l, - ) - require.Error(t, err, "fulfillment should not be generated for the request which was made on reorged fork on Simulated Chain") - }) -} - -func TestVRFv2BatchFulfillmentEnabledDisabled(t *testing.T) { - t.Parallel() - var ( - env *test_env.CLClusterTestEnv - vrfContracts *vrfcommon.VRFContracts - subIDsForCancellingAfterTest []uint64 - vrfKey *vrfcommon.VRFKeyData - nodeTypeToNodeMap map[vrfcommon.VRFNodeType]*vrfcommon.VRFNode - sethClient *seth.Client - ) - l := logging.GetTestLogger(t) - - config, err := tc.GetChainAndTestTypeSpecificConfig("Smoke", tc.VRFv2) - require.NoError(t, err, "Error getting config") - network := networks.MustGetSelectedNetworkConfig(config.GetNetworkConfig())[0] - chainID := network.ChainID - - configPtr := &config - vrfEnvConfig := vrfcommon.VRFEnvConfig{ - TestConfig: config, - ChainID: chainID, - CleanupFn: vrfv2CleanUpFn(&t, &sethClient, &configPtr, &env, &vrfContracts, &subIDsForCancellingAfterTest, nil), - } - newEnvConfig := vrfcommon.NewEnvConfig{ - NodesToCreate: []vrfcommon.VRFNodeType{vrfcommon.VRF}, - NumberOfTxKeysToCreate: 0, - UseVRFOwner: false, - UseTestCoordinator: false, - ChainlinkNodeLogScannerSettings: test_env.DefaultChainlinkNodeLogScannerSettings, - } - env, vrfContracts, vrfKey, nodeTypeToNodeMap, sethClient, err = vrfv2.SetupVRFV2Universe(testcontext.Get(t), t, vrfEnvConfig, newEnvConfig, l) - require.NoError(t, err, "Error setting up VRFv2 universe") - - // batchMaxGas := config.MaxGasLimit() (2.5 mill) + 400_000 = 2.9 mill - // callback gas limit set by consumer = 500k - // so 4 requests should be fulfilled inside 1 tx since 500k*4 < 2.9 mill - - batchFulfilmentMaxGas := *config.VRFv2.General.MaxGasLimitCoordinatorConfig + 400_000 - config.VRFv2.General.CallbackGasLimit = ptr.Ptr(uint32(500_000)) - - expectedNumberOfFulfillmentsInsideOneBatchFulfillment := (batchFulfilmentMaxGas / *config.VRFv2.General.CallbackGasLimit) - 1 - randRequestCount := expectedNumberOfFulfillmentsInsideOneBatchFulfillment - - t.Run("Batch Fulfillment Enabled", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - - vrfNode, exists := nodeTypeToNodeMap[vrfcommon.VRF] - require.True(t, exists, "VRF Node does not exist") - - // ensure that no job present on the node - err = actions.DeleteJobs([]*nodeclient.ChainlinkClient{vrfNode.CLNode.API}) - require.NoError(t, err) - - batchFullfillmentEnabled := true - // create job with batch fulfillment enabled - vrfJobSpecConfig := vrfcommon.VRFJobSpecConfig{ - ForwardingAllowed: *configCopy.VRFv2.General.VRFJobForwardingAllowed, - CoordinatorAddress: vrfContracts.CoordinatorV2.Address(), - BatchCoordinatorAddress: vrfContracts.BatchCoordinatorV2.Address(), - FromAddresses: vrfNode.TXKeyAddressStrings, - EVMChainID: strconv.FormatInt(chainID, 10), - MinIncomingConfirmations: int(*configCopy.VRFv2.General.MinimumConfirmations), - PublicKey: vrfKey.PubKeyCompressed, - EstimateGasMultiplier: *configCopy.VRFv2.General.VRFJobEstimateGasMultiplier, - BatchFulfillmentEnabled: batchFullfillmentEnabled, - BatchFulfillmentGasMultiplier: *configCopy.VRFv2.General.VRFJobBatchFulfillmentGasMultiplier, - PollPeriod: configCopy.VRFv2.General.VRFJobPollPeriod.Duration, - RequestTimeout: configCopy.VRFv2.General.VRFJobRequestTimeout.Duration, - SimulationBlock: configCopy.VRFv2.General.VRFJobSimulationBlock, - VRFOwnerConfig: &vrfcommon.VRFOwnerConfig{ - UseVRFOwner: false, - }, - } - - l.Info(). - Msg("Creating VRFV2 Job with `batchFulfillmentEnabled = true`") - job, err := vrfv2.CreateVRFV2Job( - vrfNode.CLNode.API, - vrfJobSpecConfig, - ) - require.NoError(t, err, "error creating job with higher timeout") - vrfNode.Job = job - - consumers, subIDs, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subID := subIDs[0] - subscription, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subID) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscription, strconv.FormatUint(subID, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDs...) - - if randRequestCount > math.MaxUint16 { - t.Fatalf("rand request count overflows uint16: %d", randRequestCount) - } - configCopy.VRFv2.General.RandomnessRequestCountPerRequest = ptr.Ptr(uint16(randRequestCount)) - - // test and assert - _, randomWordsFulfilledEvent, err := vrfv2.RequestRandomnessAndWaitForFulfillment( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subID, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - configCopy.VRFv2.General.RandomWordsFulfilledEventTimeout.Duration, - 0, - ) - require.NoError(t, err, "error requesting randomness and waiting for fulfilment") - - var wgAllRequestsFulfilled sync.WaitGroup - wgAllRequestsFulfilled.Add(1) - requestCount, fulfilmentCount, err := vrfcommon.WaitForRequestCountEqualToFulfilmentCount(testcontext.Get(t), consumers[0], 2*time.Minute, &wgAllRequestsFulfilled) - require.NoError(t, err) - wgAllRequestsFulfilled.Wait() - - l.Info(). - Interface("Request Count", requestCount). - Interface("Fulfilment Count", fulfilmentCount). - Msg("Request/Fulfilment Stats") - - clNodeTxs, resp, err := nodeTypeToNodeMap[vrfcommon.VRF].CLNode.API.ReadTransactions() - require.NoError(t, err) - require.Equal(t, 200, resp.StatusCode) - var batchFulfillmentTxs []nodeclient.TransactionData - for _, tx := range clNodeTxs.Data { - if common.HexToAddress(tx.Attributes.To).Cmp(common.HexToAddress(vrfContracts.BatchCoordinatorV2.Address())) == 0 { - batchFulfillmentTxs = append(batchFulfillmentTxs, tx) - } - } - // verify that all fulfillments should be inside one tx - require.Len(t, batchFulfillmentTxs, 1) - - fulfillmentTx, _, err := sethClient.Client.TransactionByHash(testcontext.Get(t), randomWordsFulfilledEvent.Raw.TxHash) - require.NoError(t, err, "error getting tx from hash") - - fulfillmentTXToAddress := fulfillmentTx.To().String() - l.Info(). - Str("Actual Fulfillment Tx To Address", fulfillmentTXToAddress). - Str("BatchCoordinatorV2 Address", vrfContracts.BatchCoordinatorV2.Address()). - Msg("Fulfillment Tx To Address should be the BatchCoordinatorV2 Address when batch fulfillment is enabled") - - // verify that VRF node sends fulfillments via BatchCoordinator contract - require.Equal(t, vrfContracts.BatchCoordinatorV2.Address(), fulfillmentTXToAddress, "Fulfillment Tx To Address should be the BatchCoordinatorV2 Address when batch fulfillment is enabled") - - // verify that all fulfillments should be inside one tx - // This check is disabled for live testnets since each testnet has different gas usage for similar tx - if network.Simulated { - fulfillmentTxReceipt, err := sethClient.Client.TransactionReceipt(testcontext.Get(t), fulfillmentTx.Hash()) - require.NoError(t, err) - randomWordsFulfilledLogs, err := contracts.ParseRandomWordsFulfilledLogs(vrfContracts.CoordinatorV2, fulfillmentTxReceipt.Logs) - require.NoError(t, err) - require.Len(t, batchFulfillmentTxs, 1) - require.Len(t, randomWordsFulfilledLogs, int(randRequestCount)) - } - }) - t.Run("Batch Fulfillment Disabled", func(t *testing.T) { - configCopy := config.MustCopy().(tc.TestConfig) - - vrfNode, exists := nodeTypeToNodeMap[vrfcommon.VRF] - require.True(t, exists, "VRF Node does not exist") - // ensure that no job present on the node - err = actions.DeleteJobs([]*nodeclient.ChainlinkClient{vrfNode.CLNode.API}) - require.NoError(t, err) - - batchFullfillmentEnabled := false - - // create job with batchFulfillmentEnabled = false - vrfJobSpecConfig := vrfcommon.VRFJobSpecConfig{ - ForwardingAllowed: *configCopy.VRFv2.General.VRFJobForwardingAllowed, - CoordinatorAddress: vrfContracts.CoordinatorV2.Address(), - BatchCoordinatorAddress: vrfContracts.BatchCoordinatorV2.Address(), - FromAddresses: vrfNode.TXKeyAddressStrings, - EVMChainID: strconv.FormatInt(chainID, 10), - MinIncomingConfirmations: int(*configCopy.VRFv2.General.MinimumConfirmations), - PublicKey: vrfKey.PubKeyCompressed, - EstimateGasMultiplier: *configCopy.VRFv2.General.VRFJobEstimateGasMultiplier, - BatchFulfillmentEnabled: batchFullfillmentEnabled, - BatchFulfillmentGasMultiplier: *configCopy.VRFv2.General.VRFJobBatchFulfillmentGasMultiplier, - PollPeriod: configCopy.VRFv2.General.VRFJobPollPeriod.Duration, - RequestTimeout: configCopy.VRFv2.General.VRFJobRequestTimeout.Duration, - SimulationBlock: configCopy.VRFv2.General.VRFJobSimulationBlock, - VRFOwnerConfig: &vrfcommon.VRFOwnerConfig{ - UseVRFOwner: false, - }, - } - - l.Info(). - Msg("Creating VRFV2 Job with `batchFulfillmentEnabled = false`") - job, err := vrfv2.CreateVRFV2Job( - vrfNode.CLNode.API, - vrfJobSpecConfig, - ) - require.NoError(t, err, "error creating job with higher timeout") - vrfNode.Job = job - - consumers, subIDs, err := vrfv2.SetupNewConsumersAndSubs( - sethClient, - vrfContracts.CoordinatorV2, - configCopy, - vrfContracts.LinkToken, - 1, - 1, - l, - ) - require.NoError(t, err, "error setting up new consumers and subs") - subID := subIDs[0] - subscription, err := vrfContracts.CoordinatorV2.GetSubscription(testcontext.Get(t), subID) - require.NoError(t, err, "error getting subscription information") - vrfcommon.LogSubDetails(l, subscription, strconv.FormatUint(subID, 10), vrfContracts.CoordinatorV2) - subIDsForCancellingAfterTest = append(subIDsForCancellingAfterTest, subIDs...) - - if randRequestCount > math.MaxUint16 { - t.Fatalf("rand request count overflows uint16: %d", randRequestCount) - } - configCopy.VRFv2.General.RandomnessRequestCountPerRequest = ptr.Ptr(uint16(randRequestCount)) - - // test and assert - _, randomWordsFulfilledEvent, err := vrfv2.RequestRandomnessAndWaitForFulfillment( - l, - consumers[0], - vrfContracts.CoordinatorV2, - subID, - vrfKey, - *configCopy.VRFv2.General.MinimumConfirmations, - *configCopy.VRFv2.General.CallbackGasLimit, - *configCopy.VRFv2.General.NumberOfWords, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequest, - *configCopy.VRFv2.General.RandomnessRequestCountPerRequestDeviation, - configCopy.VRFv2.General.RandomWordsFulfilledEventTimeout.Duration, - 0, - ) - require.NoError(t, err, "error requesting randomness and waiting for fulfilment") - - var wgAllRequestsFulfilled sync.WaitGroup - wgAllRequestsFulfilled.Add(1) - requestCount, fulfilmentCount, err := vrfcommon.WaitForRequestCountEqualToFulfilmentCount(testcontext.Get(t), consumers[0], 2*time.Minute, &wgAllRequestsFulfilled) - require.NoError(t, err) - wgAllRequestsFulfilled.Wait() - - l.Info(). - Interface("Request Count", requestCount). - Interface("Fulfilment Count", fulfilmentCount). - Msg("Request/Fulfilment Stats") - - fulfillmentTx, _, err := sethClient.Client.TransactionByHash(testcontext.Get(t), randomWordsFulfilledEvent.Raw.TxHash) - require.NoError(t, err, "error getting tx from hash") - - fulfillmentTXToAddress := fulfillmentTx.To().String() - l.Info(). - Str("Actual Fulfillment Tx To Address", fulfillmentTXToAddress). - Str("CoordinatorV2 Address", vrfContracts.CoordinatorV2.Address()). - Msg("Fulfillment Tx To Address should be the CoordinatorV2 Address when batch fulfillment is disabled") - - // verify that VRF node sends fulfillments via Coordinator contract - require.Equal(t, vrfContracts.CoordinatorV2.Address(), fulfillmentTXToAddress, "Fulfillment Tx To Address should be the CoordinatorV2 Address when batch fulfillment is disabled") - - clNodeTxs, resp, err := nodeTypeToNodeMap[vrfcommon.VRF].CLNode.API.ReadTransactions() - require.NoError(t, err) - require.Equal(t, 200, resp.StatusCode) - - var singleFulfillmentTxs []nodeclient.TransactionData - for _, tx := range clNodeTxs.Data { - if common.HexToAddress(tx.Attributes.To).Cmp(common.HexToAddress(vrfContracts.CoordinatorV2.Address())) == 0 { - singleFulfillmentTxs = append(singleFulfillmentTxs, tx) - } - } - // verify that all fulfillments should be in separate txs - require.Len(t, singleFulfillmentTxs, int(randRequestCount)) - }) -} diff --git a/package.json b/package.json index dd3f1262e04..86a073b3db7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chainlink", - "version": "2.39.0", + "version": "2.40.0", "description": "node of the decentralized oracle network, bridging on and off-chain computation", "main": "index.js", "scripts": { diff --git a/plugins/chainlink.Dockerfile b/plugins/chainlink.Dockerfile index eacc305aa83..9b6420ed7b5 100644 --- a/plugins/chainlink.Dockerfile +++ b/plugins/chainlink.Dockerfile @@ -88,6 +88,10 @@ USER ${CHAINLINK_USER} # Copy Delve debugger from build stage. COPY --from=buildgo /go/bin/dlv /usr/local/bin/dlv +# Expose image metadata to the running node. +ARG CL_AUTO_DOCKER_TAG=unset +ENV CL_DOCKER_TAG=${CL_AUTO_DOCKER_TAG} + # Set plugin environment variable configuration. ENV CL_MEDIAN_CMD=chainlink-feeds ARG CL_SOLANA_CMD=chainlink-solana diff --git a/plugins/plugins.private.yaml b/plugins/plugins.private.yaml index d78f7987747..33989e2939e 100644 --- a/plugins/plugins.private.yaml +++ b/plugins/plugins.private.yaml @@ -52,5 +52,5 @@ plugins: installPath: "." confidential-http: - moduleURI: "github.com/smartcontractkit/confidential-compute/enclave/apps/confidential-http/capability" - gitRef: "6e03ab2b759f6a6983673e4071b712438b1c923c" + gitRef: "187018af88ce265ca70b86222f8587e3fcb7ff32" installPath: "./cmd/confidential-http" diff --git a/plugins/plugins.public.yaml b/plugins/plugins.public.yaml index e0b53817230..48419e4a942 100644 --- a/plugins/plugins.public.yaml +++ b/plugins/plugins.public.yaml @@ -35,7 +35,7 @@ plugins: solana: - moduleURI: "github.com/smartcontractkit/chainlink-solana" - gitRef: "v1.1.2-0.20260320011913-f2205f8506c7" + gitRef: "v1.1.2-0.20260325152920-167c7a34804c" installPath: "./pkg/solana/cmd/chainlink-solana" starknet: diff --git a/system-tests/lib/cre/features/vault/vault.go b/system-tests/lib/cre/features/vault/vault.go index 9eefa2db649..ba5b1bada00 100644 --- a/system-tests/lib/cre/features/vault/vault.go +++ b/system-tests/lib/cre/features/vault/vault.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "strconv" + "time" "dario.cat/mergo" "github.com/Masterminds/semver/v3" @@ -13,6 +14,7 @@ import ( "github.com/pelletier/go-toml/v2" "github.com/pkg/errors" "github.com/rs/zerolog" + "google.golang.org/protobuf/types/known/durationpb" chainselectors "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/smdkg/dkgocr/dkgocrtypes" @@ -105,7 +107,8 @@ func (o *Vault) PreEnvStartup( CapabilityType: 1, // ACTION }, Config: &capabilitiespb.CapabilityConfig{ - LocalOnly: don.HasOnlyLocalCapabilities(), + LocalOnly: don.HasOnlyLocalCapabilities(), + MethodConfigs: vaultMethodConfigs(), }, }} @@ -385,6 +388,20 @@ func reportingPluginConfigOverride(vaultDKGOCR3Addr *common.Address, creEnv *cre return cfgb, nil } +func vaultMethodConfigs() map[string]*capabilitiespb.CapabilityMethodConfig { + return map[string]*capabilitiespb.CapabilityMethodConfig{ + vaultprotos.MethodGetSecrets: { + RemoteConfig: &capabilitiespb.CapabilityMethodConfig_RemoteExecutableConfig{ + RemoteExecutableConfig: &capabilitiespb.RemoteExecutableConfig{ + RequestTimeout: durationpb.New(2 * time.Minute), + ServerMaxParallelRequests: 10, + RequestHasherType: capabilitiespb.RequestHasherType_Simple, + }, + }, + }, + } +} + func EncryptSecret(secret, masterPublicKeyStr string, owner common.Address) (string, error) { masterPublicKey := tdh2easy.PublicKey{} masterPublicKeyBytes, err := hex.DecodeString(masterPublicKeyStr) diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index 0f160122227..fc71a5e3de2 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -32,15 +32,15 @@ require ( github.com/sethvargo/go-retry v0.3.0 github.com/smartcontractkit/chain-selectors v1.0.97 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 - github.com/smartcontractkit/chainlink-common v0.11.0 + github.com/smartcontractkit/chainlink-common v0.11.1 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260320152158-2191d797b5ce github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f - github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 + github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.5 github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.15 github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.10.0 @@ -470,7 +470,7 @@ require ( github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706 // indirect github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 // indirect diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index ef145d68aa1..ad246f62ccf 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1601,8 +1601,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7db87ae70/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.0 h1:b/6fGMruUCKqxxzNBmTjCupRkd+m6LqvPCBBMTkpxU0= -github.com/smartcontractkit/chainlink-common v0.11.0/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.11.1 h1:JVTnqoQjdLDmQQXNgssmzEQnJK0gQ/0427LqS4UDuqE= +github.com/smartcontractkit/chainlink-common v0.11.1/go.mod h1:9W8E7tfchAsrSNHdMM1mzLmle+bL1P8Ou0I4LG1qNxw= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1639,14 +1639,14 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0. github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:ATjAPIVJibHRcIfiG47rEQkUIOoYa6KDvWj3zwCAw6g= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d h1:AJy55QJ/pBhXkZjc7N+ATnWfxrcjq9BI9DmdtdjwDUQ= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 h1:03tbcwjyIEjvHba1IWOj1sfThwebm2XNzyFHSuZtlWc= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 h1:fkS5FJpSozwxL2FA6OJDi7az2DrtMNiK1X5DWuHDyfA= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 h1:hhevsu8k7tlDRrYZmgAh7V4avGQDMvus1bwIlial3Ps= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 h1:oli+2uLU6jcrJGCuYFqk3475hiwL17SWlITWLv+tx/w= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 h1:AEnxv4HM3WD1RbQkRiFyb9cJ6YKAcqBp1CpIcFdZfuo= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= @@ -1661,8 +1661,8 @@ github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f h1:3+vQMwuWL6+OqNutFqo/+gkczJwcr+MBPqeSxcjfI1Y= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 h1:XLMJ6FDQoEiqDNZ4B1MV9Vi1lL8vOfo9SzgqkM8IiuA= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c h1:7MUil5RQBxxnmfwp2bc1N4jv/8FVLH0hAkJupnGNMCg= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075 h1:PcR7Zdh+Z+Dh/S4lQ1xDbnFrb6He70KW9O5+9DtgloE= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075/go.mod h1:APCV5fIW/a+JGM+Cz9yb6XyGt8ht5hISEYfpG/k4Z+k= github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1:4mGJySR1GAJAAFRwEo6YiSKM2zSHzYT5b/FSmrpNUGI= @@ -1781,6 +1781,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/suzuki-shunsuke/go-convmap v0.2.1 h1:g94CxI6ENYluXZhdEH+1WVGhMAE8nLvAmWLUCwBw6W0= +github.com/suzuki-shunsuke/go-convmap v0.2.1/go.mod h1:3XfGRbtyNBMGfXAxhROSRki6/UIlUX31Qt6DvdI6lUs= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index fe7d81913a6..de03684c6e1 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -39,6 +39,8 @@ replace github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/ replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana/solwrite => ./smoke/cre/solana/solwrite +replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/vaultsecret => ./smoke/cre/vaultsecret + require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/avast/retry-go/v4 v4.7.0 @@ -54,13 +56,13 @@ require ( github.com/rs/zerolog v1.34.0 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-common v0.11.0 + github.com/smartcontractkit/chainlink-common v0.11.1 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 github.com/smartcontractkit/chainlink-data-streams v0.1.13 github.com/smartcontractkit/chainlink-deployments-framework v0.86.3 github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260107191744-4b93f62cffe3 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706 github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f @@ -135,8 +137,8 @@ require ( github.com/smartcontractkit/chainlink-protos/chainlink-ccv/heartbeat v0.0.0-20260115142640-f6b99095c12e // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect - github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect + github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c // indirect github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075 // indirect github.com/stellar/go-stellar-sdk v0.1.0 // indirect github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 // indirect @@ -608,6 +610,7 @@ require ( github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260218133534-cbd44da2856b // indirect github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/httpaction-negative v0.0.0-20251015074515-1acc1d3fb4c0 github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/httpaction v0.0.0-20251015074515-1acc1d3fb4c0 + github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/vaultsecret v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/smartcontractkit/mcms v0.38.2 // indirect diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 997e20dbb1d..63710cf0e90 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1785,8 +1785,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260317185256-d5f7db87ae70/go.mod h1:P0/tjeeIIxfsBupk5MneRjq5uI9mj+ZQpMpYnFla6WM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2 h1:5HdH/A6yn8INZAltYDLb7UkUi5IKemhJzJkDW4Bgxyg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260324000441-d4cfddc9f7d2/go.mod h1:wDHq2E0KwUWG0lQ9f5frW1a7CKVW17MJLPuvKmtSRDg= -github.com/smartcontractkit/chainlink-common v0.11.0 h1:b/6fGMruUCKqxxzNBmTjCupRkd+m6LqvPCBBMTkpxU0= -github.com/smartcontractkit/chainlink-common v0.11.0/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-common v0.11.1 h1:JVTnqoQjdLDmQQXNgssmzEQnJK0gQ/0427LqS4UDuqE= +github.com/smartcontractkit/chainlink-common v0.11.1/go.mod h1:9W8E7tfchAsrSNHdMM1mzLmle+bL1P8Ou0I4LG1qNxw= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 h1:NOUsjsMzNecbjiPWUQGlRSRAutEvCFrqqyETDJeh5q4= @@ -1823,14 +1823,14 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0. github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:ATjAPIVJibHRcIfiG47rEQkUIOoYa6KDvWj3zwCAw6g= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d h1:AJy55QJ/pBhXkZjc7N+ATnWfxrcjq9BI9DmdtdjwDUQ= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 h1:03tbcwjyIEjvHba1IWOj1sfThwebm2XNzyFHSuZtlWc= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 h1:fkS5FJpSozwxL2FA6OJDi7az2DrtMNiK1X5DWuHDyfA= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 h1:hhevsu8k7tlDRrYZmgAh7V4avGQDMvus1bwIlial3Ps= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 h1:oli+2uLU6jcrJGCuYFqk3475hiwL17SWlITWLv+tx/w= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 h1:AEnxv4HM3WD1RbQkRiFyb9cJ6YKAcqBp1CpIcFdZfuo= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= @@ -1845,8 +1845,8 @@ github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f h1:3+vQMwuWL6+OqNutFqo/+gkczJwcr+MBPqeSxcjfI1Y= github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7 h1:XLMJ6FDQoEiqDNZ4B1MV9Vi1lL8vOfo9SzgqkM8IiuA= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260320011913-f2205f8506c7/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c h1:7MUil5RQBxxnmfwp2bc1N4jv/8FVLH0hAkJupnGNMCg= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c/go.mod h1:tHAxfvRGFtttKFw4YnMwRLgawWLNWVfPbL0Wl07wuP8= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075 h1:PcR7Zdh+Z+Dh/S4lQ1xDbnFrb6He70KW9O5+9DtgloE= github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260217175957-8f1af02c5075/go.mod h1:APCV5fIW/a+JGM+Cz9yb6XyGt8ht5hISEYfpG/k4Z+k= github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1:4mGJySR1GAJAAFRwEo6YiSKM2zSHzYT5b/FSmrpNUGI= @@ -1977,6 +1977,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/suzuki-shunsuke/go-convmap v0.2.1 h1:g94CxI6ENYluXZhdEH+1WVGhMAE8nLvAmWLUCwBw6W0= +github.com/suzuki-shunsuke/go-convmap v0.2.1/go.mod h1:3XfGRbtyNBMGfXAxhROSRki6/UIlUX31Qt6DvdI6lUs= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= diff --git a/system-tests/tests/smoke/cre/v2_vault_don_test.go b/system-tests/tests/smoke/cre/v2_vault_don_test.go index fdf1695e31c..2bb459ed8bb 100644 --- a/system-tests/tests/smoke/cre/v2_vault_don_test.go +++ b/system-tests/tests/smoke/cre/v2_vault_don_test.go @@ -16,15 +16,22 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/google/uuid" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" vault_helpers "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/vault" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" jsonrpc "github.com/smartcontractkit/chainlink-common/pkg/jsonrpc2" + capabilities_registry_v2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/capabilities_registry_wrapper_v2" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + commonevents "github.com/smartcontractkit/chainlink-protos/workflows/go/common" + workflowevents "github.com/smartcontractkit/chainlink-protos/workflows/go/events" + ctfblockchain "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/smartcontractkit/chainlink-testing-framework/seth" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" crecontracts "github.com/smartcontractkit/chainlink/system-tests/lib/cre/contracts" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/evm" + vaultsecret_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/vaultsecret/config" t_helpers "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers" "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaulttypes" @@ -33,15 +40,13 @@ import ( "github.com/smartcontractkit/chainlink/system-tests/lib/cre" crevault "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/vault" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/vault" + creworkflow "github.com/smartcontractkit/chainlink/system-tests/lib/cre/workflow" ttypes "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers/configuration" "github.com/smartcontractkit/chainlink-testing-framework/framework" ) func ExecuteVaultTest(t *testing.T, testEnv *ttypes.TestEnvironment) { - /* - BUILD ENVIRONMENT FROM SAVED STATE - */ var testLogger = framework.L testLogger.Info().Msgf("Ensuring DKG result packages are present...") @@ -68,34 +73,84 @@ func ExecuteVaultTest(t *testing.T, testEnv *ttypes.TestEnvironment) { require.NoError(t, err, "failed to parse gateway URL") testLogger.Info().Msgf("Gateway URL: %s", gatewayURL.String()) - workflowRegistryAddress := crecontracts.MustGetAddressFromDataStore(testEnv.CreEnvironment.CldfEnvironment.DataStore, testEnv.CreEnvironment.Blockchains[0].ChainSelector(), keystone_changeset.WorkflowRegistry.String(), testEnv.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()], "") - require.IsType(t, &evm.Blockchain{}, testEnv.CreEnvironment.Blockchains[0], "expected EVM blockchain type") - sethClient := testEnv.CreEnvironment.Blockchains[0].(*evm.Blockchain).SethClient - ownerAddr := sethClient.MustGetRootKeyAddress().Hex() - workflowName := t_helpers.UniqueWorkflowName(testEnv, "consensustest") - t_helpers.CompileAndDeployWorkflow(t, testEnv, testLogger, workflowName, &t_helpers.None{}, "../../../../core/scripts/cre/environment/examples/workflows/v2/node-mode/main.go") - wfRegistryContract, err := workflow_registry_v2_wrapper.NewWorkflowRegistry(common.HexToAddress(workflowRegistryAddress), sethClient.Client) - require.NoError(t, err, "failed to get workflow registry contract wrapper") - - secretID := strconv.Itoa(rand.Intn(10000)) // generate a random secret ID for testing - secretValue := "Secret Value to be stored" vaultPublicKey := FetchVaultPublicKey(t, gatewayURL.String()) - encryptedSecret, err := crevault.EncryptSecret(secretValue, vaultPublicKey, sethClient.MustGetRootKeyAddress()) - require.NoError(t, err, "failed to encrypt secret") - - // Wait for the node to be up. - framework.L.Info().Msg("Waiting 30 seconds for the Vault DON to be ready...") - time.Sleep(30 * time.Second) - executeVaultSecretsCreateTest(t, encryptedSecret, secretID, ownerAddr, gatewayURL.String(), sethClient, wfRegistryContract) - // disable get tests - // executeVaultSecretsGetTest(t, secretID, ownerAddr, gatewayURL.String(), sethClient, wfRegistryContract) - executeVaultSecretsUpdateTest(t, encryptedSecret, secretID, ownerAddr, gatewayURL.String(), sethClient, wfRegistryContract) - executeVaultSecretsListTest(t, secretID, ownerAddr, gatewayURL.String(), sethClient, wfRegistryContract) - executeVaultSecretsDeleteTest(t, secretID, ownerAddr, gatewayURL.String(), sethClient, wfRegistryContract) + updateVaultCapabilityConfigInRegistry(t, testEnv, vaultPublicKey) + + gwURL := gatewayURL.String() + + t.Run("basic_crud", func(t *testing.T) { + if parallelEnabled && fanoutEnabled { + t.Parallel() + } + subEnv := t_helpers.SetupTestEnvironmentWithPerTestKeys(t, testEnv.TestConfig) + sc := subEnv.CreEnvironment.Blockchains[0].(*evm.Blockchain).SethClient + owner := sc.MustGetRootKeyAddress().Hex() + wfRegAddr := crecontracts.MustGetAddressFromDataStore(subEnv.CreEnvironment.CldfEnvironment.DataStore, subEnv.CreEnvironment.Blockchains[0].ChainSelector(), keystone_changeset.WorkflowRegistry.String(), subEnv.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()], "") + wfReg, err := workflow_registry_v2_wrapper.NewWorkflowRegistry(common.HexToAddress(wfRegAddr), sc.Client) + require.NoError(t, err) + require.NoError(t, creworkflow.LinkOwner(sc, common.HexToAddress(wfRegAddr), subEnv.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()])) + secretID := strconv.Itoa(rand.Intn(10000)) + enc, err := crevault.EncryptSecret("secret-basic", vaultPublicKey, sc.MustGetRootKeyAddress()) + require.NoError(t, err) + ulCh := make(chan *workflowevents.UserLogs, 1000) + bmCh := make(chan *commonevents.BaseMessage, 1000) + sink := t_helpers.StartChipTestSink(t, t_helpers.GetPublishFn(testLogger, ulCh, bmCh)) + t.Cleanup(func() { sink.Shutdown(t.Context()); close(ulCh); close(bmCh) }) + + executeVaultSecretsCreateTest(t, enc, secretID, owner, gwURL, "main", sc, wfReg) + executeVaultSecretsGetViaWorkflowTest(t, subEnv, "bget1", secretID, "main", ulCh, bmCh) + executeVaultSecretsUpdateTest(t, enc, secretID, owner, gwURL, "main", sc, wfReg) + executeVaultSecretsGetViaWorkflowTest(t, subEnv, "bget2", secretID, "main", ulCh, bmCh) + executeVaultSecretsListTest(t, secretID, owner, gwURL, "main", sc, wfReg) + executeVaultSecretsDeleteTest(t, secretID, owner, gwURL, "main", sc, wfReg) + executeVaultSecretsGetNotFoundViaWorkflowTest(t, subEnv, "bdel1", secretID, "main", ulCh, bmCh) + }) + + t.Run("cross_namespace", func(t *testing.T) { + if parallelEnabled && fanoutEnabled { + t.Parallel() + } + subEnv := t_helpers.SetupTestEnvironmentWithPerTestKeys(t, testEnv.TestConfig) + sc := subEnv.CreEnvironment.Blockchains[0].(*evm.Blockchain).SethClient + owner := sc.MustGetRootKeyAddress().Hex() + wfRegAddr := crecontracts.MustGetAddressFromDataStore(subEnv.CreEnvironment.CldfEnvironment.DataStore, subEnv.CreEnvironment.Blockchains[0].ChainSelector(), keystone_changeset.WorkflowRegistry.String(), subEnv.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()], "") + wfReg, err := workflow_registry_v2_wrapper.NewWorkflowRegistry(common.HexToAddress(wfRegAddr), sc.Client) + require.NoError(t, err) + require.NoError(t, creworkflow.LinkOwner(sc, common.HexToAddress(wfRegAddr), subEnv.CreEnvironment.ContractVersions[keystone_changeset.WorkflowRegistry.String()])) + secretID := strconv.Itoa(rand.Intn(10000)) + enc, err := crevault.EncryptSecret("secret-xns", vaultPublicKey, sc.MustGetRootKeyAddress()) + require.NoError(t, err) + ulCh := make(chan *workflowevents.UserLogs, 1000) + bmCh := make(chan *commonevents.BaseMessage, 1000) + sink := t_helpers.StartChipTestSink(t, t_helpers.GetPublishFn(testLogger, ulCh, bmCh)) + t.Cleanup(func() { sink.Shutdown(t.Context()); close(ulCh); close(bmCh) }) + + altNS := "alt" + + executeVaultSecretsCreateTest(t, enc, secretID, owner, gwURL, "main", sc, wfReg) + executeVaultSecretsCreateTest(t, enc, secretID, owner, gwURL, altNS, sc, wfReg) + + executeVaultSecretsGetViaWorkflowTest(t, subEnv, "xget1", secretID, "main", ulCh, bmCh) + executeVaultSecretsGetViaWorkflowTest(t, subEnv, "xgeta1", secretID, altNS, ulCh, bmCh) + + executeVaultSecretsUpdateTest(t, enc, secretID, owner, gwURL, "main", sc, wfReg) + executeVaultSecretsGetViaWorkflowTest(t, subEnv, "xget2", secretID, "main", ulCh, bmCh) + executeVaultSecretsGetViaWorkflowTest(t, subEnv, "xgeta2", secretID, altNS, ulCh, bmCh) + + executeVaultSecretsListTest(t, secretID, owner, gwURL, "main", sc, wfReg) + executeVaultSecretsListTest(t, secretID, owner, gwURL, altNS, sc, wfReg) + + executeVaultSecretsDeleteTest(t, secretID, owner, gwURL, "main", sc, wfReg) + executeVaultSecretsGetNotFoundViaWorkflowTest(t, subEnv, "xdel1", secretID, "main", ulCh, bmCh) + executeVaultSecretsGetViaWorkflowTest(t, subEnv, "xgeta3", secretID, altNS, ulCh, bmCh) + + executeVaultSecretsDeleteTest(t, secretID, owner, gwURL, altNS, sc, wfReg) + executeVaultSecretsGetNotFoundViaWorkflowTest(t, subEnv, "xdela1", secretID, altNS, ulCh, bmCh) + }) } -func executeVaultSecretsCreateTest(t *testing.T, encryptedSecret, secretID, owner, gatewayURL string, sethClient *seth.Client, wfRegistryContract *workflow_registry_v2_wrapper.WorkflowRegistry) { - framework.L.Info().Msg("Creating secret...") +func executeVaultSecretsCreateTest(t *testing.T, encryptedSecret, secretID, owner, gatewayURL, namespace string, sethClient *seth.Client, wfRegistryContract *workflow_registry_v2_wrapper.WorkflowRegistry) { + framework.L.Info().Msgf("Creating secret (namespace=%s)...", namespace) uniqueRequestID := uuid.New().String() @@ -106,7 +161,7 @@ func executeVaultSecretsCreateTest(t *testing.T, encryptedSecret, secretID, owne Id: &vault_helpers.SecretIdentifier{ Key: secretID, Owner: owner, - Namespace: "main", + Namespace: namespace, }, EncryptedValue: encryptedSecret, }, @@ -155,13 +210,56 @@ func executeVaultSecretsCreateTest(t *testing.T, encryptedSecret, secretID, owne require.Empty(t, result0.GetError()) require.Equal(t, secretID, result0.GetId().Key) require.Equal(t, owner, result0.GetId().Owner) - require.Equal(t, vaulttypes.DefaultNamespace, result0.GetId().Namespace) + require.Equal(t, namespace, result0.GetId().Namespace) - framework.L.Info().Msg("Secret created successfully") + framework.L.Info().Msgf("Secret created successfully (namespace=%s)", namespace) } -func executeVaultSecretsUpdateTest(t *testing.T, encryptedSecret, secretID, owner, gatewayURL string, sethClient *seth.Client, wfRegistryContract *workflow_registry_v2_wrapper.WorkflowRegistry) { - framework.L.Info().Msg("Updating secret...") +func executeVaultSecretsGetViaWorkflowTest( + t *testing.T, testEnv *ttypes.TestEnvironment, + workflowBaseName, secretKey, secretNamespace string, + userLogsCh chan *workflowevents.UserLogs, baseMessageCh chan *commonevents.BaseMessage, +) { + testLogger := framework.L + testLogger.Info().Msgf("Verifying secret retrieval via workflow (key=%s, namespace=%s)...", secretKey, secretNamespace) + + workflowName := t_helpers.UniqueWorkflowName(testEnv, workflowBaseName) + cfg := &vaultsecret_config.Config{ + SecretKey: secretKey, + SecretNamespace: secretNamespace, + } + const workflowFileLocation = "./vaultsecret/main.go" + workflowID := t_helpers.CompileAndDeployWorkflow(t, testEnv, testLogger, workflowName, cfg, workflowFileLocation) + + expectedLog := "Vault secret retrieved successfully via workflow" + t_helpers.WatchWorkflowLogs(t, testLogger, userLogsCh, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, expectedLog, 4*time.Minute, t_helpers.WithUserLogWorkflowID(workflowID)) + testLogger.Info().Msg("Vault secret get via workflow test completed") +} + +func executeVaultSecretsGetNotFoundViaWorkflowTest( + t *testing.T, testEnv *ttypes.TestEnvironment, + workflowBaseName, secretKey, secretNamespace string, + userLogsCh chan *workflowevents.UserLogs, baseMessageCh chan *commonevents.BaseMessage, +) { + testLogger := framework.L + testLogger.Info().Msgf("Verifying secret is NOT retrievable via workflow after deletion (key=%s, namespace=%s)...", secretKey, secretNamespace) + + workflowName := t_helpers.UniqueWorkflowName(testEnv, workflowBaseName) + cfg := &vaultsecret_config.Config{ + SecretKey: secretKey, + SecretNamespace: secretNamespace, + ExpectNotFound: true, + } + const workflowFileLocation = "./vaultsecret/main.go" + workflowID := t_helpers.CompileAndDeployWorkflow(t, testEnv, testLogger, workflowName, cfg, workflowFileLocation) + + expectedLog := "Vault secret correctly not found after deletion" + t_helpers.WatchWorkflowLogs(t, testLogger, userLogsCh, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, expectedLog, 4*time.Minute, t_helpers.WithUserLogWorkflowID(workflowID)) + testLogger.Info().Msg("Vault secret not-found via workflow test completed") +} + +func executeVaultSecretsUpdateTest(t *testing.T, encryptedSecret, secretID, owner, gatewayURL, namespace string, sethClient *seth.Client, wfRegistryContract *workflow_registry_v2_wrapper.WorkflowRegistry) { + framework.L.Info().Msgf("Updating secret (namespace=%s)...", namespace) uniqueRequestID := uuid.New().String() secretsUpdateRequest := vault_helpers.UpdateSecretsRequest{ @@ -171,7 +269,7 @@ func executeVaultSecretsUpdateTest(t *testing.T, encryptedSecret, secretID, owne Id: &vault_helpers.SecretIdentifier{ Key: secretID, Owner: owner, - Namespace: "main", + Namespace: namespace, }, EncryptedValue: encryptedSecret, }, @@ -179,7 +277,7 @@ func executeVaultSecretsUpdateTest(t *testing.T, encryptedSecret, secretID, owne Id: &vault_helpers.SecretIdentifier{ Key: "invalid", Owner: owner, - Namespace: "main", + Namespace: namespace, }, EncryptedValue: encryptedSecret, }, @@ -230,21 +328,21 @@ func executeVaultSecretsUpdateTest(t *testing.T, encryptedSecret, secretID, owne require.Empty(t, result0.GetError()) require.Equal(t, secretID, result0.GetId().Key) require.Equal(t, owner, result0.GetId().Owner) - require.Equal(t, vaulttypes.DefaultNamespace, result0.GetId().Namespace) + require.Equal(t, namespace, result0.GetId().Namespace) result1 := updateSecretsResponse.GetResponses()[1] require.Contains(t, result1.Error, "key does not exist") - framework.L.Info().Msg("Secret updated successfully") + framework.L.Info().Msgf("Secret updated successfully (namespace=%s)", namespace) } -func executeVaultSecretsListTest(t *testing.T, secretID, owner, gatewayURL string, sethClient *seth.Client, wfRegistryContract *workflow_registry_v2_wrapper.WorkflowRegistry) { - framework.L.Info().Msg("Listing secret...") +func executeVaultSecretsListTest(t *testing.T, secretID, owner, gatewayURL, namespace string, sethClient *seth.Client, wfRegistryContract *workflow_registry_v2_wrapper.WorkflowRegistry) { + framework.L.Info().Msgf("Listing secrets (namespace=%s)...", namespace) uniqueRequestID := uuid.New().String() secretsListRequest := vault_helpers.ListSecretIdentifiersRequest{ RequestId: uniqueRequestID, Owner: owner, - Namespace: "main", + Namespace: namespace, } secretsListRequestBody, err := json.Marshal(secretsListRequest) //nolint:govet // The lock field is not set on this proto require.NoError(t, err, "failed to marshal secrets request") @@ -262,7 +360,7 @@ func executeVaultSecretsListTest(t *testing.T, secretID, owner, gatewayURL strin secretsListRequestTwo := vault_helpers.ListSecretIdentifiersRequest{ RequestId: uniqueRequestIDTwo, Owner: owner, - Namespace: "main", + Namespace: namespace, } secretsListRequestBodyTwo, err := json.Marshal(secretsListRequestTwo) //nolint:govet // The lock field is not set on this proto require.NoError(t, err, "failed to marshal secrets request") @@ -327,14 +425,14 @@ func executeVaultSecretsListTest(t *testing.T, secretID, owner, gatewayURL strin for _, identifier := range listSecretsResponse.Identifiers { keys = append(keys, identifier.Key) require.Equal(t, owner, identifier.Owner) - require.Equal(t, vaulttypes.DefaultNamespace, identifier.Namespace) + require.Equal(t, namespace, identifier.Namespace) } require.Contains(t, keys, secretID) - framework.L.Info().Msg("Secrets listed successfully") + framework.L.Info().Msgf("Secrets listed successfully (namespace=%s)", namespace) } -func executeVaultSecretsDeleteTest(t *testing.T, secretID, owner, gatewayURL string, sethClient *seth.Client, wfRegistryContract *workflow_registry_v2_wrapper.WorkflowRegistry) { - framework.L.Info().Msg("Deleting secret...") +func executeVaultSecretsDeleteTest(t *testing.T, secretID, owner, gatewayURL, namespace string, sethClient *seth.Client, wfRegistryContract *workflow_registry_v2_wrapper.WorkflowRegistry) { + framework.L.Info().Msgf("Deleting secret (namespace=%s)...", namespace) uniqueRequestID := uuid.New().String() secretsDeleteRequest := vault_helpers.DeleteSecretsRequest{ @@ -343,12 +441,12 @@ func executeVaultSecretsDeleteTest(t *testing.T, secretID, owner, gatewayURL str { Key: secretID, Owner: owner, - Namespace: "main", + Namespace: namespace, }, { Key: "invalid", Owner: owner, - Namespace: "main", + Namespace: namespace, }, }, } @@ -400,7 +498,99 @@ func executeVaultSecretsDeleteTest(t *testing.T, secretID, owner, gatewayURL str result1 := deleteSecretsResponse.GetResponses()[1] require.Contains(t, result1.Error, "key does not exist") - framework.L.Info().Msg("Secrets deleted successfully") + framework.L.Info().Msgf("Secrets deleted successfully (namespace=%s)", namespace) +} + +// updateVaultCapabilityConfigInRegistry updates the on-chain capabilities registry +// so that the vault@1.0.0 capability config includes DefaultConfig with VaultPublicKey +// and Threshold. This is required for workflows that call runtime.GetSecret(). +// Uses the original deployer key (not per-test key) since the registry is owned by the deployer. +func updateVaultCapabilityConfigInRegistry(t *testing.T, testEnv *ttypes.TestEnvironment, vaultPublicKey string) { + t.Helper() + testLogger := framework.L + testLogger.Info().Msg("Updating vault capability config in capabilities registry with VaultPublicKey...") + + capRegAddr := crecontracts.MustGetAddressFromDataStore( + testEnv.CreEnvironment.CldfEnvironment.DataStore, + testEnv.CreEnvironment.RegistryChainSelector, + keystone_changeset.CapabilitiesRegistry.String(), + testEnv.CreEnvironment.ContractVersions[keystone_changeset.CapabilitiesRegistry.String()], + "", + ) + + require.IsType(t, &evm.Blockchain{}, testEnv.CreEnvironment.Blockchains[0]) + sethClient := testEnv.CreEnvironment.Blockchains[0].(*evm.Blockchain).SethClient + + deployerClient, err := seth.NewClientBuilder(). + WithRpcUrl(sethClient.URL). + WithPrivateKeys([]string{ctfblockchain.DefaultAnvilPrivateKey}). + WithProtections(false, false, seth.MustMakeDuration(time.Second)). + Build() + require.NoError(t, err, "failed to create deployer seth client") + + capReg, err := capabilities_registry_v2.NewCapabilitiesRegistry( + common.HexToAddress(capRegAddr), deployerClient.Client, + ) + require.NoError(t, err, "failed to create capabilities registry wrapper") + + allDONs, err := capReg.GetDONs(&bind.CallOpts{}, big.NewInt(0), big.NewInt(100)) + require.NoError(t, err, "failed to get DONs from registry") + + var don *capabilities_registry_v2.CapabilitiesRegistryDONInfo + for i := range allDONs { + for _, cc := range allDONs[i].CapabilityConfigurations { + if cc.CapabilityId == "vault@1.0.0" { + don = &allDONs[i] + break + } + } + if don != nil { + break + } + } + require.NotNil(t, don, "could not find a DON with vault@1.0.0 capability in the registry") + testLogger.Info().Msgf("Found vault capability on DON %q (ID=%d)", don.Name, don.Id) + + newConfigs := make([]capabilities_registry_v2.CapabilitiesRegistryCapabilityConfiguration, 0, len(don.CapabilityConfigurations)) + for _, cc := range don.CapabilityConfigurations { + if cc.CapabilityId == "vault@1.0.0" { + existingConfig := &capabilitiespb.CapabilityConfig{} + if len(cc.Config) > 0 { + require.NoError(t, proto.Unmarshal(cc.Config, existingConfig), "failed to unmarshal existing vault capability config") + } + + vaultCfg := map[string]interface{}{ + "VaultPublicKey": vaultPublicKey, + "Threshold": 1, + } + valueMap, wrapErr := values.WrapMap(vaultCfg) + require.NoError(t, wrapErr, "failed to wrap vault config values") + + existingConfig.DefaultConfig = values.ProtoMap(valueMap) + + configBytes, marshalErr := proto.Marshal(existingConfig) + require.NoError(t, marshalErr, "failed to marshal updated vault capability config") + + cc.Config = configBytes + testLogger.Info().Msg("Injected VaultPublicKey and Threshold into vault@1.0.0 capability config") + } + newConfigs = append(newConfigs, cc) + } + + updateParams := capabilities_registry_v2.CapabilitiesRegistryUpdateDONParams{ + Name: don.Name, + Nodes: don.NodeP2PIds, + CapabilityConfigurations: newConfigs, + IsPublic: don.IsPublic, + F: don.F, + Config: don.Config, + } + + _, err = deployerClient.Decode(capReg.UpdateDONByName(deployerClient.NewTXOpts(), don.Name, updateParams)) + require.NoError(t, err, "UpdateDONByName tx failed") + + testLogger.Info().Msg("Waiting for registry syncer to propagate the on-chain config change...") + time.Sleep(15 * time.Second) // registry syncer polls every 12s; one tick + margin } func allowlistRequest(t *testing.T, owner string, request jsonrpc.Request[json.RawMessage], sethClient *seth.Client, wfRegistryContract *workflow_registry_v2_wrapper.WorkflowRegistry) { @@ -413,7 +603,6 @@ func allowlistRequest(t *testing.T, owner string, request jsonrpc.Request[json.R require.NoError(t, err, "failed to allowlist request") framework.L.Info().Msgf("Allowlisting request digest at contract %s, for owner: %s, digestHexStr: %s", wfRegistryContract.Address().Hex(), owner, requestDigest) - time.Sleep(10 * time.Second) // wait a bit to ensure the allowlist is propagated onchain, gateway and vault don nodes allowedList, err := wfRegistryContract.GetAllowlistedRequests(&bind.CallOpts{}, big.NewInt(0), big.NewInt(100)) require.NoError(t, err, "failed to validate allowlisted request") for _, req := range allowedList { diff --git a/system-tests/tests/smoke/cre/v2_vault_don_test_helpers.go b/system-tests/tests/smoke/cre/v2_vault_don_test_helpers.go index a9a493e2e8a..1b3976c857c 100644 --- a/system-tests/tests/smoke/cre/v2_vault_don_test_helpers.go +++ b/system-tests/tests/smoke/cre/v2_vault_don_test_helpers.go @@ -5,6 +5,7 @@ import ( "encoding/json" "io" "net/http" + "strings" "testing" "time" @@ -56,20 +57,56 @@ func FetchVaultPublicKey(t *testing.T, gatewayURL string) (publicKey string) { } func sendVaultRequestToGateway(t *testing.T, gatewayURL string, requestBody []byte) (statusCode int, body []byte) { + const maxRetries = 7 + const retryInterval = 2 * time.Second + framework.L.Info().Msgf("Request Body: %s", string(requestBody)) - req, err := http.NewRequestWithContext(t.Context(), "POST", gatewayURL, bytes.NewBuffer(requestBody)) - require.NoError(t, err, "failed to create request") - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") + for attempt := range maxRetries + 1 { + req, err := http.NewRequestWithContext(t.Context(), "POST", gatewayURL, bytes.NewBuffer(requestBody)) + require.NoError(t, err, "failed to create request") + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err, "failed to execute request") - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err, "failed to execute request") - defer resp.Body.Close() + body, err = io.ReadAll(resp.Body) + resp.Body.Close() + require.NoError(t, err, "failed to read http response body") + statusCode = resp.StatusCode - body, err = io.ReadAll(resp.Body) - require.NoError(t, err, "failed to read http response body") - framework.L.Info().Msgf("HTTP Response Body: %s", string(body)) - return resp.StatusCode, body + framework.L.Info().Msgf("HTTP Response Body: %s", string(body)) + + if !isGatewayNotAllowlistedError(body) { + return statusCode, body + } + + if attempt < maxRetries { + framework.L.Warn().Msgf("Request not yet allowlisted, retrying in %s (attempt %d/%d)...", retryInterval, attempt+1, maxRetries) + time.Sleep(retryInterval) + } + } + + return statusCode, body +} + +// isGatewayNotAllowlistedError checks whether the response is a gateway-level +// "request not allowlisted" rejection (method is empty, error code -32600). +// Node-level rejections (method is set, code -32603) have a different format +// and must not be retried because the gateway has already consumed the request. +func isGatewayNotAllowlistedError(body []byte) bool { + var resp struct { + Method string `json:"method"` + Error *struct { + Message string `json:"message"` + } `json:"error"` + } + if json.Unmarshal(body, &resp) != nil { + return false + } + return resp.Method == "" && resp.Error != nil && + strings.Contains(resp.Error.Message, "request not allowlisted") } diff --git a/system-tests/tests/smoke/cre/vaultsecret/config/config.go b/system-tests/tests/smoke/cre/vaultsecret/config/config.go new file mode 100644 index 00000000000..9b057eb7d17 --- /dev/null +++ b/system-tests/tests/smoke/cre/vaultsecret/config/config.go @@ -0,0 +1,7 @@ +package config + +type Config struct { + SecretKey string `yaml:"secretKey"` + SecretNamespace string `yaml:"secretNamespace"` + ExpectNotFound bool `yaml:"expectNotFound"` +} diff --git a/system-tests/tests/smoke/cre/vaultsecret/go.mod b/system-tests/tests/smoke/cre/vaultsecret/go.mod new file mode 100644 index 00000000000..7f42b4ae6b4 --- /dev/null +++ b/system-tests/tests/smoke/cre/vaultsecret/go.mod @@ -0,0 +1,22 @@ +module github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/vaultsecret + +go 1.25.7 + +require ( + github.com/smartcontractkit/cre-sdk-go v1.5.0 + github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 // indirect + github.com/stretchr/testify v1.11.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/system-tests/tests/smoke/cre/vaultsecret/go.sum b/system-tests/tests/smoke/cre/vaultsecret/go.sum new file mode 100644 index 00000000000..e26a567397f --- /dev/null +++ b/system-tests/tests/smoke/cre/vaultsecret/go.sum @@ -0,0 +1,37 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 h1:03tbcwjyIEjvHba1IWOj1sfThwebm2XNzyFHSuZtlWc= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/cre-sdk-go v1.5.0 h1:kepW3QDKARrOOHjXwWAZ9j5KLk6bxLzvi6OMrLsFwVo= +github.com/smartcontractkit/cre-sdk-go v1.5.0/go.mod h1:yYrQFz1UH7hhRbPO0q4fgo1tfsJNd4yXnI3oCZE0RzM= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.0 h1:qBZ4y6qlTOynSpU1QAi2Fgr3tUZQ332b6hit9EVZqkk= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.0/go.mod h1:Rzhy75vD3FqQo/SV6lypnxIwjWac6IOWzI5BYj3tYMU= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/system-tests/tests/smoke/cre/vaultsecret/main.go b/system-tests/tests/smoke/cre/vaultsecret/main.go new file mode 100644 index 00000000000..b1233563cbe --- /dev/null +++ b/system-tests/tests/smoke/cre/vaultsecret/main.go @@ -0,0 +1,76 @@ +//go:build wasip1 + +package main + +import ( + "fmt" + "log/slog" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" + + "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/vaultsecret/config" +) + +func main() { + wasm.NewRunner(func(b []byte) (config.Config, error) { + cfg := config.Config{} + if err := yaml.Unmarshal(b, &cfg); err != nil { + return config.Config{}, fmt.Errorf("error unmarshalling config: %w", err) + } + return cfg, nil + }).Run(RunVaultSecretWorkflow) +} + +func RunVaultSecretWorkflow(cfg config.Config, _ *slog.Logger, _ cre.SecretsProvider) (cre.Workflow[config.Config], error) { + return cre.Workflow[config.Config]{ + cre.Handler( + cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}), + onTrigger, + ), + }, nil +} + +func onTrigger(cfg config.Config, runtime cre.Runtime, _ *cron.Payload) (string, error) { + runtime.Logger().Info("Vault secret workflow triggered", + "secretKey", cfg.SecretKey, + "secretNamespace", cfg.SecretNamespace, + "expectNotFound", cfg.ExpectNotFound, + ) + + secret, err := runtime.GetSecret(&cre.SecretRequest{ + Namespace: cfg.SecretNamespace, + Id: cfg.SecretKey, + }).Await() + + if cfg.ExpectNotFound { + if err != nil && strings.Contains(err.Error(), "key does not exist") { + runtime.Logger().Info("Vault secret correctly not found after deletion", "secretKey", cfg.SecretKey) + return fmt.Sprintf("Secret correctly not found: key=%s", cfg.SecretKey), nil + } + if err != nil { + runtime.Logger().Error("Expected 'key does not exist' but got a different error", + "error", err, "secretKey", cfg.SecretKey) + return "", fmt.Errorf("expected 'key does not exist' for key=%s, but got: %w", cfg.SecretKey, err) + } + runtime.Logger().Error("Expected secret to be gone but retrieval succeeded", "secretKey", cfg.SecretKey) + return "", fmt.Errorf("expected secret key=%s to be deleted, but it was still found", cfg.SecretKey) + } + + if err != nil { + runtime.Logger().Error("Failed to get secret via workflow", "error", err) + return "", fmt.Errorf("failed to get secret: %w", err) + } + + if secret.Value == "" { + runtime.Logger().Error("Secret value is empty") + return "", fmt.Errorf("secret value is empty for key=%s namespace=%s", cfg.SecretKey, cfg.SecretNamespace) + } + + runtime.Logger().Info("Vault secret retrieved successfully via workflow", "secretKey", cfg.SecretKey) + return fmt.Sprintf("Secret retrieved: key=%s", cfg.SecretKey), nil +} diff --git a/system-tests/tests/test-helpers/t_helpers.go b/system-tests/tests/test-helpers/t_helpers.go index 257647f227f..d460e252846 100644 --- a/system-tests/tests/test-helpers/t_helpers.go +++ b/system-tests/tests/test-helpers/t_helpers.go @@ -71,6 +71,7 @@ import ( http_config "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/http/config" httpaction_negative_config "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/httpaction-negative/config" httpaction_smoke_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/httpaction/config" + vaultsecret_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/vaultsecret/config" ) const WorkflowEngineInitErrorLog = "Workflow Engine initialization failed" @@ -298,7 +299,8 @@ type WorkflowConfig interface { http_config.Config | httpaction_smoke_config.Config | httpaction_negative_config.Config | - solwrite_config.Config + solwrite_config.Config | + vaultsecret_config.Config } // None represents an empty workflow configuration @@ -463,6 +465,12 @@ func workflowConfigFactory[T WorkflowConfig](t *testing.T, testLogger zerolog.Lo workflowConfigFilePath = workflowCfgFilePath require.NoError(t, configErr, "failed to create solwrite workflow config file") testLogger.Info().Msg("Solana write workflow config file created.") + + case *vaultsecret_config.Config: + workflowCfgFilePath, configErr := CreateWorkflowYamlConfigFile(workflowName, cfg, outputDir) + workflowConfigFilePath = workflowCfgFilePath + require.NoError(t, configErr, "failed to create vaultsecret workflow config file") + testLogger.Info().Msg("Vault secret workflow config file created.") default: require.NoError(t, fmt.Errorf("unsupported workflow config type: %T", cfg)) } diff --git a/testdata/scripts/health/default.txtar b/testdata/scripts/health/default.txtar index cb20b294721..675d2561a79 100644 --- a/testdata/scripts/health/default.txtar +++ b/testdata/scripts/health/default.txtar @@ -41,6 +41,7 @@ ok LLOTransmissionReaper ok Mailbox.Monitor ok Mercury.WSRPCPool ok Mercury.WSRPCPool.CacheSet +ok NodePlatformBuildInfo ok PipelineORM ok PipelineRunner ok PipelineRunner.BridgeCache @@ -141,6 +142,15 @@ ok WorkflowStore "output": "" } }, + { + "type": "checks", + "id": "NodePlatformBuildInfo", + "attributes": { + "name": "NodePlatformBuildInfo", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "PipelineORM", diff --git a/testdata/scripts/health/multi-chain-loopp.txtar b/testdata/scripts/health/multi-chain-loopp.txtar index fee61cd072d..04e0dc87c4f 100644 --- a/testdata/scripts/health/multi-chain-loopp.txtar +++ b/testdata/scripts/health/multi-chain-loopp.txtar @@ -142,6 +142,7 @@ ok LLOTransmissionReaper ok Mailbox.Monitor ok Mercury.WSRPCPool ok Mercury.WSRPCPool.CacheSet +ok NodePlatformBuildInfo ok PipelineORM ok PipelineRunner ok PipelineRunner.BridgeCache @@ -534,6 +535,15 @@ ok WorkflowStore "output": "" } }, + { + "type": "checks", + "id": "NodePlatformBuildInfo", + "attributes": { + "name": "NodePlatformBuildInfo", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "PipelineORM", diff --git a/testdata/scripts/health/multi-chain.txtar b/testdata/scripts/health/multi-chain.txtar index caa250b4abd..ad456fc2db8 100644 --- a/testdata/scripts/health/multi-chain.txtar +++ b/testdata/scripts/health/multi-chain.txtar @@ -78,6 +78,7 @@ ok LLOTransmissionReaper ok Mailbox.Monitor ok Mercury.WSRPCPool ok Mercury.WSRPCPool.CacheSet +ok NodePlatformBuildInfo ok PipelineORM ok PipelineRunner ok PipelineRunner.BridgeCache @@ -303,6 +304,15 @@ ok WorkflowStore "output": "" } }, + { + "type": "checks", + "id": "NodePlatformBuildInfo", + "attributes": { + "name": "NodePlatformBuildInfo", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "PipelineORM", diff --git a/tools/docker/docker-compose.yaml b/tools/docker/docker-compose.yaml index 1178196256c..16afc40c47e 100644 --- a/tools/docker/docker-compose.yaml +++ b/tools/docker/docker-compose.yaml @@ -5,6 +5,8 @@ services: build: context: ../../ dockerfile: core/chainlink.Dockerfile + args: + CL_AUTO_DOCKER_TAG: latest # Note that the keystore import allows us to submit transactions # immediately because addresses are specified when starting the # parity/geth node to be prefunded with eth. @@ -26,6 +28,8 @@ services: build: context: ../../ dockerfile: core/chainlink.Dockerfile + args: + CL_AUTO_DOCKER_TAG: latest entrypoint: /bin/sh -c "chainlink -c /run/secrets/config node start -d -p /run/secrets/node_password -a /run/secrets/apicredentials" restart: always env_file: @@ -47,4 +51,3 @@ secrets: file: ../secrets/0xb90c7E3F7815F59EAD74e7543eB6D9E8538455D6.json config: file: config.toml -