diff --git a/.github/workflows/devenv-compat.yml b/.github/workflows/devenv-compat.yml index 5e37f46f665..5999ea2ba8e 100644 --- a/.github/workflows/devenv-compat.yml +++ b/.github/workflows/devenv-compat.yml @@ -17,7 +17,9 @@ on: type: boolean default: false refs: - description: "Git refs to test, should be used only for tool testing purposes. In all other cases refs are detected automatically either from Git or RANE SOT" + description: "Git refs to test, should be used only for tool testing purposes. + In all other cases refs are detected automatically either from Git or + RANE SOT" required: false type: string exclude-refs: @@ -56,6 +58,11 @@ on: required: false default: "2" type: string + tag-version-ceiling: + description: "Version ceiling for compatibility testing (tags newer than this + version will be excluded)" + required: false + type: string working-directory: description: "Working directory for commands" required: false @@ -95,13 +102,16 @@ jobs: DON_NODES: ${{ inputs.don-nodes }} NODE_NAME_TEMPLATE: ${{ inputs.node-name-template }} VERSIONS_BACK: ${{ inputs.versions-back }} + TAG_VERSION_CEILING: ${{ inputs.tag-version-ceiling }} WORKING_DIR: ${{ inputs.working-directory }} LOGS_DIR: ${{ inputs.logs-directory }} CTF_LOG_LEVEL: ${{ inputs.ctf-log-level }} # JD is required by the local CRE-based tests - CTF_JD_IMAGE: "${{ secrets.AWS_ACCOUNT_ID_PROD }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/job-distributor:0.22.1" + CTF_JD_IMAGE: "${{ secrets.AWS_ACCOUNT_ID_PROD }}.dkr.ecr.${{ + secrets.QA_AWS_REGION }}.amazonaws.com/job-distributor:0.22.1" # ChIP Router is required by the local CRE-based tests - CTF_CHIP_ROUTER_IMAGE: "${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ secrets.QA_AWS_REGION }}.amazonaws.com/local-cre-chip-router:v1.0.1" + CTF_CHIP_ROUTER_IMAGE: "${{ secrets.QA_AWS_ACCOUNT_NUMBER }}.dkr.ecr.${{ + secrets.QA_AWS_REGION }}.amazonaws.com/local-cre-chip-router:v1.0.1" steps: - name: Checkout code uses: actions/checkout@v5 @@ -129,7 +139,8 @@ jobs: id: login-ecr uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 with: - registries: ${{ format('{0},{1}', secrets.QA_AWS_ACCOUNT_NUMBER, secrets.AWS_ACCOUNT_ID_PROD) }} + registries: ${{ format('{0},{1}', secrets.QA_AWS_ACCOUNT_NUMBER, + secrets.AWS_ACCOUNT_ID_PROD) }} env: AWS_REGION: ${{ secrets.QA_AWS_REGION }} @@ -157,7 +168,7 @@ jobs: working-directory: ${{ env.WORKING_DIR }} run: | CTF_VERSION=$(go list -m -json "github.com/smartcontractkit/chainlink-testing-framework/framework" | jq -r .Version) - echo "CTF_TAG=framework/$CTF_VERSION" >> $GITHUB_ENV + echo "CTF_TAG=framework/$CTF_VERSION" >> "$GITHUB_ENV" - name: Install CTF binary working-directory: ${{ env.WORKING_DIR }} @@ -199,19 +210,26 @@ jobs: STRIP_IMAGE_SUFFIX: ${{ env.STRIP_IMAGE_SUFFIX }} PRODUCT: ${{ env.PRODUCT }} NO_GIT_ROLLBACK: ${{ env.NO_GIT_ROLLBACK }} + REFS: ${{ env.REFS }} + EXCLUDE_REFS: ${{ env.EXCLUDE_REFS }} BUILD_CMD: ${{ env.BUILD_CMD }} ENV_CMD: ${{ env.ENV_CMD }} TEST_CMD: ${{ env.TEST_CMD }} NODES: ${{ env.NODES }} VERSIONS_BACK: ${{ env.VERSIONS_BACK }} + TAG_VERSION_CEILING: ${{ env.TAG_VERSION_CEILING }} run: | - REFS_ARGS="" - if [ -n "${{ env.REFS }}" ]; then - REFS_ARGS="--refs ${{ env.REFS }}" + REFS_ARGS=() + if [ -n "${REFS}" ]; then + REFS_ARGS=(--refs "${REFS}") + fi + EXCLUDE_REFS_ARGS=() + if [ -n "${EXCLUDE_REFS}" ]; then + EXCLUDE_REFS_ARGS=(--exclude-refs "${EXCLUDE_REFS}") fi - EXCLUDE_REFS_ARGS="" - if [ -n "${{ env.EXCLUDE_REFS }}" ]; then - EXCLUDE_REFS_ARGS="--exclude-refs ${{ env.EXCLUDE_REFS }}" + ROLLBACK_FLAG=() + if [ -n "${NO_GIT_ROLLBACK}" ]; then + ROLLBACK_FLAG=("${NO_GIT_ROLLBACK}") fi ./bin/ctf compat backward \ --registry "${REGISTRY}" \ @@ -219,26 +237,29 @@ jobs: --envcmd "${ENV_CMD}" \ --testcmd "${TEST_CMD}" \ --product "${PRODUCT}" \ - ${REFS_ARGS} \ - ${EXCLUDE_REFS_ARGS} \ + "${REFS_ARGS[@]}" \ + "${EXCLUDE_REFS_ARGS[@]}" \ --upgrade-nodes "${UPGRADE_NODES}" \ --don_nodes "${DON_NODES}" \ --node-name-template "${NODE_NAME_TEMPLATE}" \ --strip-image-suffix "${STRIP_IMAGE_SUFFIX}" \ --versions-back "${VERSIONS_BACK}" \ - ${{ env.NO_GIT_ROLLBACK }} + --tag-version-ceiling "${TAG_VERSION_CEILING}" \ + "${ROLLBACK_FLAG[@]}" - name: Attach CI Summary working-directory: ${{ env.WORKING_DIR }} if: always() run: | if [ -f "ci_summary.txt" ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**CI Summary:**" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - cat ci_summary.txt >> $GITHUB_STEP_SUMMARY + { + echo "" + echo "**CI Summary:**" + echo '```' + cat ci_summary.txt + } >> "$GITHUB_STEP_SUMMARY" else - echo "ci_summary.txt not found" >> $GITHUB_STEP_SUMMARY + echo "ci_summary.txt not found" >> "$GITHUB_STEP_SUMMARY" fi - name: Collect Docker container logs @@ -246,10 +267,11 @@ jobs: working-directory: ${{ env.WORKING_DIR }} run: | mkdir -p logs - for container in $(docker ps -q); do - container_name=$(docker inspect --format='{{.Name}}' $container | sed 's/^\///') - docker logs $container > "logs/${container_name}.log" 2>&1 - done + while IFS= read -r container; do + [ -z "${container}" ] && continue + container_name=$(docker inspect --format='{{.Name}}' "${container}" | sed 's/^\///') + docker logs "${container}" > "logs/${container_name}.log" 2>&1 + done < <(docker ps -q) - name: Upload logs if: always() diff --git a/.github/workflows/post-build-publish.yml b/.github/workflows/post-build-publish.yml index af58b6af19b..3ebae6a007b 100644 --- a/.github/workflows/post-build-publish.yml +++ b/.github/workflows/post-build-publish.yml @@ -27,12 +27,17 @@ jobs: echo "Chainlink Version is required, but 'client_payload.chainlink_version' is empty" exit 1 fi + if [[ "$CHAINLINK_VERSION" != v* ]]; then + echo "This workflow can only be used with semver tags, 'chainlink_version' must begin with 'v', got: $CHAINLINK_VERSION" + exit 1 + fi echo "Trigger validated successfully" # Upgrade compatibility tests # Include rc and beta releases, since we will be upgrading from them on canary servers df1-compat: uses: ./.github/workflows/devenv-compat.yml name: Data Feeds v1 Upgrade Test + needs: validate-trigger permissions: id-token: write contents: read @@ -48,11 +53,13 @@ jobs: logs-directory: "./devenv/tests/ocr2/logs" ctf-log-level: "debug" strip-image-suffix: "v" + tag-version-ceiling: ${{ github.event.client_payload.chainlink_version }} secrets: inherit cre-compat: uses: ./.github/workflows/devenv-compat.yml name: CRE Upgrade Test + needs: validate-trigger permissions: id-token: write contents: read @@ -68,6 +75,7 @@ jobs: logs-directory: "./system-tests/tests/smoke/cre/logs" ctf-log-level: "debug" strip-image-suffix: "v" + tag-version-ceiling: ${{ github.event.client_payload.chainlink_version }} secrets: inherit legacy-system-tests: diff --git a/.github/workflows/post-image-released.yml b/.github/workflows/post-image-released.yml index 081558ce334..866cc76974c 100644 --- a/.github/workflows/post-image-released.yml +++ b/.github/workflows/post-image-released.yml @@ -2,6 +2,12 @@ name: "Post Image Released" on: # to be used with post release tests workflow_dispatch: + inputs: + tag-version-ceiling: + description: "Version ceiling for compatibility testing (tags newer than this + version will be excluded)" + required: false + type: string permissions: {} @@ -26,6 +32,7 @@ jobs: logs-directory: "./devenv/tests/ocr2/logs" ctf-log-level: "debug" strip-image-suffix: "v" + tag-version-ceiling: ${{ inputs.tag-version-ceiling }} secrets: inherit cre-compat: @@ -46,5 +53,6 @@ jobs: logs-directory: "./system-tests/tests/smoke/cre/logs" ctf-log-level: "debug" strip-image-suffix: "v" + tag-version-ceiling: ${{ inputs.tag-version-ceiling }} secrets: inherit # Do not run other tests, since they were already run on the pre-release image diff --git a/core/config/app_config.go b/core/config/app_config.go index 37755cbb265..68f2f9954a0 100644 --- a/core/config/app_config.go +++ b/core/config/app_config.go @@ -63,6 +63,7 @@ type AppConfig interface { CCV() CCV Billing() Billing BridgeStatusReporter() BridgeStatusReporter + JobSpecReporter() JobSpecReporter Sharding() Sharding LOOPP() LOOPP } diff --git a/core/config/docs/core.toml b/core/config/docs/core.toml index 62d666b08d2..078b56a0899 100644 --- a/core/config/docs/core.toml +++ b/core/config/docs/core.toml @@ -913,6 +913,16 @@ IgnoreInvalidBridges = true # Default # IgnoreJoblessBridges skips bridges that have no associated jobs. IgnoreJoblessBridges = false # Default +# JobSpecReporter holds settings for the Job Spec Reporter service, which periodically emits job spec telemetry. +[JobSpecReporter] +# Enabled enables the Job Spec Reporter service. +Enabled = false # Default +# PollingInterval is how often to emit a heartbeat event for each tracked job. +PollingInterval = "1h" # Default +# EnabledOCR2PluginTypes restricts OCR2 telemetry to jobs with these plugin types. +# An empty list disables all OCR2 telemetry. Use ["all"] to enable all OCR2 plugin types. +EnabledOCR2PluginTypes = ["median"] # Default + [CRE] # UseLocalTimeProvider should be set true if the DON Time OCR Plugin is not running UseLocalTimeProvider = true # Default diff --git a/core/config/job_spec_reporter_config.go b/core/config/job_spec_reporter_config.go new file mode 100644 index 00000000000..2f5c0e8c6f8 --- /dev/null +++ b/core/config/job_spec_reporter_config.go @@ -0,0 +1,11 @@ +package config + +import "time" + +type JobSpecReporter interface { + Enabled() bool + PollingInterval() time.Duration + // EnabledOCR2PluginTypes is the allowlist of OCR2 plugin types to emit for + // (e.g. "median"). An empty slice disables all. Use ["all"] to enable all types. + EnabledOCR2PluginTypes() []string +} diff --git a/core/config/toml/types.go b/core/config/toml/types.go index cb0ec6a9ff6..26be8d50ef4 100644 --- a/core/config/toml/types.go +++ b/core/config/toml/types.go @@ -9,6 +9,7 @@ import ( "reflect" "regexp" "strings" + "time" "github.com/google/uuid" "go.uber.org/zap/zapcore" @@ -64,6 +65,7 @@ type Core struct { CRE CreConfig `toml:",omitempty"` Billing Billing `toml:",omitempty"` BridgeStatusReporter BridgeStatusReporter `toml:",omitempty"` + JobSpecReporter JobSpecReporter `toml:",omitempty"` Sharding Sharding `toml:",omitempty"` LOOPP LOOPP `toml:",omitempty"` } @@ -110,6 +112,7 @@ func (c *Core) SetFrom(f *Core) { c.CRE.setFrom(&f.CRE) c.Billing.setFrom(&f.Billing) c.BridgeStatusReporter.setFrom(&f.BridgeStatusReporter) + c.JobSpecReporter.setFrom(&f.JobSpecReporter) c.Sharding.setFrom(&f.Sharding) c.LOOPP.setFrom(&f.LOOPP) @@ -3040,6 +3043,46 @@ func (e *BridgeStatusReporter) ValidateConfig() error { return nil } +type JobSpecReporter struct { + Enabled *bool + PollingInterval *commonconfig.Duration + EnabledOCR2PluginTypes *[]string +} + +func (e *JobSpecReporter) setFrom(f *JobSpecReporter) { + if f.Enabled != nil { + e.Enabled = f.Enabled + } + if f.PollingInterval != nil { + e.PollingInterval = f.PollingInterval + } + if f.EnabledOCR2PluginTypes != nil { + e.EnabledOCR2PluginTypes = f.EnabledOCR2PluginTypes + } +} + +func (e *JobSpecReporter) ValidateConfig() error { + if e.Enabled == nil || !*e.Enabled { + return nil + } + + if e.PollingInterval == nil { + defaultInterval := commonconfig.MustNewDuration(time.Hour) + e.PollingInterval = defaultInterval + } + + if e.PollingInterval.Duration() < config.MinimumPollingInterval { + return configutils.ErrInvalid{Name: "PollingInterval", Value: e.PollingInterval.Duration(), Msg: "must be greater than or equal to: " + config.MinimumPollingInterval.String()} + } + + if e.EnabledOCR2PluginTypes == nil { + defaultTypes := []string{"median"} + e.EnabledOCR2PluginTypes = &defaultTypes + } + + return nil +} + type JobDistributor struct { DisplayName *string } diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 8f6bcfd8992..f46c857ea31 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -53,7 +53,7 @@ require ( github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260421142741-9c7fbaf7c828 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735 github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 - github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.19 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21 github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.22 github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.5 github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 @@ -486,7 +486,7 @@ 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/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d // indirect + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb // indirect github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 // indirect github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260408145530-22e2d05695cd // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc // indirect @@ -498,13 +498,14 @@ require ( github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135 // indirect github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect 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/data-feeds v0.1.1-0.20260501174546-2e8846986b36 // 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-20260430172634-acccf17ece83 // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index e4ab15e127b..51cb8e4d05f 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1635,8 +1635,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.98 h1:fuI7CQ1o5cX64eO4/LvwtfhdpGFH5vnsM/bFHRwEiww= github.com/smartcontractkit/chain-selectors v1.0.98/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d h1:KMZmEzlZ+iknVFCuv+8o9iEKK52HXyeauIWbo+KhYv8= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb h1:6UjHnVanvb+6yJuefhyVlfv6YKFGMeZY5jv+a7Sexyo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb/go.mod h1:FEm5fvIQe5O8Qdx6GvQcXsk7rDFpmYdIWXea5i4tpjw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 h1:p0nFrTYrOQzDhWYm6suaM5CoWiXV5NV7llHnp6/Kn/8= @@ -1679,8 +1679,8 @@ github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-202604231355 github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c h1:0c+bCKo47vy/ItRtGa3S/vCpE5LRlgXpGnVKQX8TgjE= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c h1:1VVreRcffo3N3zF1JVtgS+YC4puuj3y0FU0Fta7L3U0= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 h1:v/rAtObo9zMpQDnGQ0seaSEqw60JUjowwcNw3DCpVY4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 h1:nXU0s4WAVU2cAR76Ke7h9z55NuEtRq1WvT4wVEs7jwk= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= @@ -1695,6 +1695,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251 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-20260505131349-78e491b80735 h1:5bxDnwI0wuPoC0H5H3H2n9CnQPb5iakR6UmAY4j8KUg= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 h1:SG+wAsNyAcA6Kk19ljuxi3HK9Ll2lpHik8OKoY4x7A0= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY= 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= @@ -1723,8 +1725,8 @@ github.com/smartcontractkit/chainlink-sui v0.0.0-20260429183453-39df0198aed8 h1: github.com/smartcontractkit/chainlink-sui v0.0.0-20260429183453-39df0198aed8/go.mod h1:k1HSbHyPaQWPOj6lXDIAe04EuwbC5ge1nK+cpG2E8hE= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260427132612-76b9f754a556 h1:6ocsoNPu3T0LsBiZ1tGZrjhKu8pGC1opUFz5KgHALSU= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260427132612-76b9f754a556/go.mod h1:LkUo0a46JWaCsLY4SCV5ZOESudehe2RR62C1S46iOqw= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.19 h1:inTH0/PrEaVv4iLdGsdcrP/rX7KMrq/Roosr5nIA8io= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.19/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21 h1:lQs+r0+Jz9vFhmWPSo4bh0F1AePGIN7j++ex9cI4jA4= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework/components/chiprouter v1.0.3 h1:57wap/rDhBcw6+Ld7MqwQmXt2BTyVeNGvvPFDJcO8jQ= github.com/smartcontractkit/chainlink-testing-framework/framework/components/chiprouter v1.0.3/go.mod h1:MjUJAyU+kLvLLPRPBs3X7zYadds5umZcjTLHjfNhpUc= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.22 h1:V+3clQPZ0/N8PJhJDkl00giFtsf9lv5XQJl0SCGWzcM= diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index 83922a84f5b..d2fae426464 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -7,6 +7,7 @@ import ( "fmt" "math/big" "net/http" + "os" "strconv" "sync" "time" @@ -70,6 +71,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/keystore" "github.com/smartcontractkit/chainlink/v2/core/services/llo/retirement" "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/bridgestatus" + "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec" "github.com/smartcontractkit/chainlink/v2/core/services/ocr" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2" "github.com/smartcontractkit/chainlink/v2/core/services/ocrbootstrap" @@ -790,8 +792,9 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err srvcs = append(srvcs, jobSpawner, pipelineRunner) var feedsService feeds.Service + var feedsORM feeds.ORM if cfg.Feature().FeedsManager() { - feedsORM := feeds.NewORM(opts.DS, globalLogger) + feedsORM = feeds.NewORM(opts.DS, globalLogger) feedsService = feeds.NewService( feedsORM, jobORM, @@ -814,6 +817,19 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err feedsService = &feeds.NullService{} } + hostname, _ := os.Hostname() + jobSpecReporter := jobspec.NewJobSpecReporter( + cfg.JobSpecReporter(), + jobSpawner, + feedsORM, + beholder.GetEmitter(), + csaPubKeyHex, + static.Version, + hostname, + globalLogger, + ) + srvcs = append(srvcs, jobSpecReporter) + for _, s := range srvcs { if s == nil { panic("service unexpectedly nil") diff --git a/core/services/chainlink/config_general.go b/core/services/chainlink/config_general.go index 3eb77367607..9a4f4fe6a23 100644 --- a/core/services/chainlink/config_general.go +++ b/core/services/chainlink/config_general.go @@ -591,6 +591,10 @@ func (g *generalConfig) BridgeStatusReporter() coreconfig.BridgeStatusReporter { return &bridgeStatusReporterConfig{c: g.c.BridgeStatusReporter} } +func (g *generalConfig) JobSpecReporter() coreconfig.JobSpecReporter { + return &jobSpecReporterConfig{c: g.c.JobSpecReporter} +} + func (g *generalConfig) Sharding() coreconfig.Sharding { return &shardingConfig{s: g.c.Sharding} } diff --git a/core/services/chainlink/config_job_spec_reporter.go b/core/services/chainlink/config_job_spec_reporter.go new file mode 100644 index 00000000000..687d6e735bb --- /dev/null +++ b/core/services/chainlink/config_job_spec_reporter.go @@ -0,0 +1,35 @@ +package chainlink + +import ( + "time" + + "github.com/smartcontractkit/chainlink/v2/core/config" + "github.com/smartcontractkit/chainlink/v2/core/config/toml" +) + +var _ config.JobSpecReporter = (*jobSpecReporterConfig)(nil) + +type jobSpecReporterConfig struct { + c toml.JobSpecReporter +} + +func (e *jobSpecReporterConfig) Enabled() bool { + if e.c.Enabled == nil { + return false + } + return *e.c.Enabled +} + +func (e *jobSpecReporterConfig) PollingInterval() time.Duration { + if e.c.PollingInterval == nil { + return time.Hour + } + return e.c.PollingInterval.Duration() +} + +func (e *jobSpecReporterConfig) EnabledOCR2PluginTypes() []string { + if e.c.EnabledOCR2PluginTypes == nil { + return []string{"median"} + } + return *e.c.EnabledOCR2PluginTypes +} diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index 911c88d8e1e..df3f0953097 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -595,6 +595,12 @@ func TestConfig_Marshal(t *testing.T) { IgnoreInvalidBridges: ptr(true), IgnoreJoblessBridges: ptr(false), } + enabledOCR2PluginTypes := []string{"median"} + full.JobSpecReporter = toml.JobSpecReporter{ + Enabled: ptr(true), + PollingInterval: commoncfg.MustNewDuration(time.Hour), + EnabledOCR2PluginTypes: &enabledOCR2PluginTypes, + } full.Sharding = toml.Sharding{ ShardingEnabled: ptr(false), ArbiterPort: ptr[uint16](9876), diff --git a/core/services/chainlink/mocks/general_config.go b/core/services/chainlink/mocks/general_config.go index a7fbb69d415..e1f781a3924 100644 --- a/core/services/chainlink/mocks/general_config.go +++ b/core/services/chainlink/mocks/general_config.go @@ -1295,6 +1295,53 @@ func (_c *GeneralConfig_JobPipeline_Call) RunAndReturn(run func() config.JobPipe return _c } +// JobSpecReporter provides a mock function with no fields +func (_m *GeneralConfig) JobSpecReporter() config.JobSpecReporter { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for JobSpecReporter") + } + + var r0 config.JobSpecReporter + if rf, ok := ret.Get(0).(func() config.JobSpecReporter); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(config.JobSpecReporter) + } + } + + return r0 +} + +// GeneralConfig_JobSpecReporter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobSpecReporter' +type GeneralConfig_JobSpecReporter_Call struct { + *mock.Call +} + +// JobSpecReporter is a helper method to define mock.On call +func (_e *GeneralConfig_Expecter) JobSpecReporter() *GeneralConfig_JobSpecReporter_Call { + return &GeneralConfig_JobSpecReporter_Call{Call: _e.mock.On("JobSpecReporter")} +} + +func (_c *GeneralConfig_JobSpecReporter_Call) Run(run func()) *GeneralConfig_JobSpecReporter_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *GeneralConfig_JobSpecReporter_Call) Return(_a0 config.JobSpecReporter) *GeneralConfig_JobSpecReporter_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *GeneralConfig_JobSpecReporter_Call) RunAndReturn(run func() config.JobSpecReporter) *GeneralConfig_JobSpecReporter_Call { + _c.Call.Return(run) + return _c +} + // LOOPP provides a mock function with no fields func (_m *GeneralConfig) LOOPP() config.LOOPP { ret := _m.Called() diff --git a/core/services/chainlink/testdata/config-empty-effective.toml b/core/services/chainlink/testdata/config-empty-effective.toml index a073a30ed45..84fef8a54a1 100644 --- a/core/services/chainlink/testdata/config-empty-effective.toml +++ b/core/services/chainlink/testdata/config-empty-effective.toml @@ -384,6 +384,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/core/services/chainlink/testdata/config-full.toml b/core/services/chainlink/testdata/config-full.toml index 7583f0ec595..953565dc714 100644 --- a/core/services/chainlink/testdata/config-full.toml +++ b/core/services/chainlink/testdata/config-full.toml @@ -422,6 +422,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = true +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/core/services/chainlink/testdata/config-multi-chain-effective.toml b/core/services/chainlink/testdata/config-multi-chain-effective.toml index ad20742feed..3f055aa2c40 100644 --- a/core/services/chainlink/testdata/config-multi-chain-effective.toml +++ b/core/services/chainlink/testdata/config-multi-chain-effective.toml @@ -384,6 +384,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/core/services/feeds/mocks/orm.go b/core/services/feeds/mocks/orm.go index 6ce30a0a78c..7b527b0727c 100644 --- a/core/services/feeds/mocks/orm.go +++ b/core/services/feeds/mocks/orm.go @@ -1034,6 +1034,65 @@ func (_c *ORM_GetJobProposal_Call) RunAndReturn(run func(context.Context, int64) return _c } +// GetJobProposalByExternalJobID provides a mock function with given fields: ctx, externalJobID +func (_m *ORM) GetJobProposalByExternalJobID(ctx context.Context, externalJobID uuid.UUID) (*feeds.JobProposal, error) { + ret := _m.Called(ctx, externalJobID) + + if len(ret) == 0 { + panic("no return value specified for GetJobProposalByExternalJobID") + } + + var r0 *feeds.JobProposal + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (*feeds.JobProposal, error)); ok { + return rf(ctx, externalJobID) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *feeds.JobProposal); ok { + r0 = rf(ctx, externalJobID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*feeds.JobProposal) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, externalJobID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ORM_GetJobProposalByExternalJobID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJobProposalByExternalJobID' +type ORM_GetJobProposalByExternalJobID_Call struct { + *mock.Call +} + +// GetJobProposalByExternalJobID is a helper method to define mock.On call +// - ctx context.Context +// - externalJobID uuid.UUID +func (_e *ORM_Expecter) GetJobProposalByExternalJobID(ctx interface{}, externalJobID interface{}) *ORM_GetJobProposalByExternalJobID_Call { + return &ORM_GetJobProposalByExternalJobID_Call{Call: _e.mock.On("GetJobProposalByExternalJobID", ctx, externalJobID)} +} + +func (_c *ORM_GetJobProposalByExternalJobID_Call) Run(run func(ctx context.Context, externalJobID uuid.UUID)) *ORM_GetJobProposalByExternalJobID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uuid.UUID)) + }) + return _c +} + +func (_c *ORM_GetJobProposalByExternalJobID_Call) Return(_a0 *feeds.JobProposal, _a1 error) *ORM_GetJobProposalByExternalJobID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ORM_GetJobProposalByExternalJobID_Call) RunAndReturn(run func(context.Context, uuid.UUID) (*feeds.JobProposal, error)) *ORM_GetJobProposalByExternalJobID_Call { + _c.Call.Return(run) + return _c +} + // GetJobProposalByRemoteUUID provides a mock function with given fields: ctx, _a1 func (_m *ORM) GetJobProposalByRemoteUUID(ctx context.Context, _a1 uuid.UUID) (*feeds.JobProposal, error) { ret := _m.Called(ctx, _a1) diff --git a/core/services/feeds/orm.go b/core/services/feeds/orm.go index c0edd00a3a3..a6b6015667f 100644 --- a/core/services/feeds/orm.go +++ b/core/services/feeds/orm.go @@ -40,6 +40,7 @@ type ORM interface { DeleteProposal(ctx context.Context, id int64) error GetJobProposal(ctx context.Context, id int64) (*JobProposal, error) GetJobProposalByRemoteUUID(ctx context.Context, uuid uuid.UUID) (*JobProposal, error) + GetJobProposalByExternalJobID(ctx context.Context, externalJobID uuid.UUID) (*JobProposal, error) ListJobProposalsByManagersIDs(ctx context.Context, ids []int64) ([]JobProposal, error) UpdateJobProposalStatus(ctx context.Context, id int64, status JobProposalStatus) error // NEEDED? UpsertJobProposal(ctx context.Context, jp *JobProposal) (int64, error) @@ -432,6 +433,21 @@ AND status <> $2; return jp, errors.Wrap(err, "GetJobProposalByRemoteUUID failed") } +// GetJobProposalByExternalJobID gets a non-deleted job proposal by its external job ID. +func (o *orm) GetJobProposalByExternalJobID(ctx context.Context, externalJobID uuid.UUID) (jp *JobProposal, err error) { + stmt := ` +SELECT * +FROM job_proposals +WHERE external_job_id = $1 +AND status <> $2 +LIMIT 1; +` + + jp = new(JobProposal) + err = o.ds.GetContext(ctx, jp, stmt, externalJobID, JobProposalStatusDeleted) + return jp, errors.Wrap(err, "GetJobProposalByExternalJobID failed") +} + // ListJobProposalsByManagersIDs gets job proposals by feeds managers IDs. func (o *orm) ListJobProposalsByManagersIDs(ctx context.Context, ids []int64) ([]JobProposal, error) { stmt := ` diff --git a/core/services/feeds/orm_test.go b/core/services/feeds/orm_test.go index bb2009399a6..b131c98001c 100644 --- a/core/services/feeds/orm_test.go +++ b/core/services/feeds/orm_test.go @@ -610,6 +610,28 @@ func Test_ORM_GetJobProposal(t *testing.T) { _, err = orm.GetJobProposalByRemoteUUID(ctx, uuid.New()) require.Error(t, err) }) + + t.Run("by external job id", func(t *testing.T) { + externalJobID := uuid.New() + createJob(t, orm.db, externalJobID) + jpID := createJobProposal(t, orm, feeds.JobProposalStatusPending, fmID) + specID := createJobSpec(t, orm, jpID) + + err := orm.ApproveSpec(ctx, specID, externalJobID) + require.NoError(t, err) + + actual, err := orm.GetJobProposalByExternalJobID(ctx, externalJobID) + require.NoError(t, err) + + assert.Equal(t, jpID, actual.ID) + assert.Equal(t, uuid.NullUUID{UUID: externalJobID, Valid: true}, actual.ExternalJobID) + assert.Equal(t, feeds.JobProposalStatusApproved, actual.Status) + + require.NoError(t, orm.DeleteProposal(ctx, jpID)) + + _, err = orm.GetJobProposalByExternalJobID(ctx, externalJobID) + require.Error(t, err) + }) } func Test_ORM_CountJobProposalsByStatus(t *testing.T) { diff --git a/core/services/job/mocks/spawner.go b/core/services/job/mocks/spawner.go index 28ff46461b1..22b6013d4be 100644 --- a/core/services/job/mocks/spawner.go +++ b/core/services/job/mocks/spawner.go @@ -348,6 +348,39 @@ func (_c *Spawner_Ready_Call) RunAndReturn(run func() error) *Spawner_Ready_Call return _c } +// RegisterListener provides a mock function with given fields: l +func (_m *Spawner) RegisterListener(l job.Listener) { + _m.Called(l) +} + +// Spawner_RegisterListener_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RegisterListener' +type Spawner_RegisterListener_Call struct { + *mock.Call +} + +// RegisterListener is a helper method to define mock.On call +// - l job.Listener +func (_e *Spawner_Expecter) RegisterListener(l interface{}) *Spawner_RegisterListener_Call { + return &Spawner_RegisterListener_Call{Call: _e.mock.On("RegisterListener", l)} +} + +func (_c *Spawner_RegisterListener_Call) Run(run func(l job.Listener)) *Spawner_RegisterListener_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(job.Listener)) + }) + return _c +} + +func (_c *Spawner_RegisterListener_Call) Return() *Spawner_RegisterListener_Call { + _c.Call.Return() + return _c +} + +func (_c *Spawner_RegisterListener_Call) RunAndReturn(run func(job.Listener)) *Spawner_RegisterListener_Call { + _c.Run(run) + return _c +} + // Start provides a mock function with given fields: _a0 func (_m *Spawner) Start(_a0 context.Context) error { ret := _m.Called(_a0) diff --git a/core/services/job/spawner.go b/core/services/job/spawner.go index e883fb23b47..c0b1fdf5968 100644 --- a/core/services/job/spawner.go +++ b/core/services/job/spawner.go @@ -17,6 +17,13 @@ import ( ) type ( + // Listener is notified when the Spawner starts or stops a job. + // Callbacks run asynchronously and must not block. + Listener interface { + AfterJobStarted(ctx context.Context, jb Job) + AfterJobStopped(ctx context.Context, jb Job) + } + // Spawner manages the spinning up and down of the long-running // services that perform the work described by job specs. Each active job spec // has 1 or more of these services associated with it. @@ -35,6 +42,10 @@ type ( // NOTE: Prefer to use CreateJob, this is only publicly exposed for use in tests // to start a job that was previously manually inserted into DB StartService(ctx context.Context, spec Job) error + + // RegisterListener adds l to the set of listeners notified on job start/stop. + // Safe to call before or after Start. + RegisterListener(l Listener) } Checker interface { @@ -52,6 +63,9 @@ type ( activeJobsMu sync.RWMutex lggr logger.Logger + listeners []Listener + listenersMu sync.RWMutex + chStop services.StopChan lbDependentAwaiters []utils.DependentAwaiter } @@ -274,6 +288,7 @@ func (js *spawner) CreateJob(ctx context.Context, ds sqlutil.DataSource, jb *Job js.lggr.Errorw("Error starting job services", "type", jb.Type, "jobID", jb.ID, "err", err) } else { js.lggr.Infow("Started job services", "type", jb.Type, "jobID", jb.ID) + js.notifyStarted(*jb) } delegate.AfterJobCreated(*jb) @@ -340,6 +355,7 @@ func (js *spawner) DeleteJob(ctx context.Context, ds sqlutil.DataSource, jobID i if exists { // Stop the service and remove the job from memory, which will always happen even if closing the services fail. js.stopService(jobID) + js.notifyStopped(aj.spec) } lggr.Infow("Stopped and deleted job") @@ -357,6 +373,48 @@ func (js *spawner) ActiveJobs() map[int32]Job { return m } +func (js *spawner) RegisterListener(l Listener) { + js.listenersMu.Lock() + defer js.listenersMu.Unlock() + js.listeners = append(js.listeners, l) + js.lggr.Debugf("Registered job listener %T", l) +} + +func (js *spawner) notifyStarted(jb Job) { + js.dispatchToListeners(func(ctx context.Context, l Listener) { l.AfterJobStarted(ctx, jb) }) +} + +func (js *spawner) notifyStopped(jb Job) { + js.dispatchToListeners(func(ctx context.Context, l Listener) { l.AfterJobStopped(ctx, jb) }) +} + +// dispatchToListeners fans out fn to every registered listener in a single +// best-effort goroutine. Panics are recovered so a faulty listener cannot +// bring the spawner down. +func (js *spawner) dispatchToListeners(fn func(context.Context, Listener)) { + js.listenersMu.RLock() + ls := make([]Listener, len(js.listeners)) + copy(ls, js.listeners) + js.listenersMu.RUnlock() + + if len(ls) == 0 { + return + } + + ctx, cancel := js.chStop.NewCtx() + go func() { + defer cancel() + defer func() { + if r := recover(); r != nil { + js.lggr.Errorw("Panic in job spawner listener", "recover", r) + } + }() + for _, l := range ls { + fn(ctx, l) + } + }() +} + func (js *spawner) activeJobIDs() []int32 { js.activeJobsMu.RLock() defer js.activeJobsMu.RUnlock() diff --git a/core/services/nodestatusreporter/jobspec/events/emit.go b/core/services/nodestatusreporter/jobspec/events/emit.go new file mode 100644 index 00000000000..cb97783c0a2 --- /dev/null +++ b/core/services/nodestatusreporter/jobspec/events/emit.go @@ -0,0 +1,34 @@ +package events + +import ( + "context" + "fmt" + "time" + + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" +) + +func EmitJobSpecEvent(ctx context.Context, emitter beholder.Emitter, event *JobSpecEvent) error { + if event.Timestamp == "" { + event.Timestamp = time.Now().Format(time.RFC3339Nano) + } + + eventBytes, err := proto.Marshal(event) + if err != nil { + return fmt.Errorf("failed to marshal JobSpecEvent: %w", err) + } + + err = emitter.Emit(ctx, eventBytes, + "partitionkey", event.ExternalJobId, + "beholder_data_schema", SchemaJobSpec, + "beholder_domain", BeholderDomain, + "beholder_entity", fmt.Sprintf("%s.%s", ProtoPkg, JobSpecEventEntity), + ) + if err != nil { + return fmt.Errorf("failed to emit JobSpecEvent: %w", err) + } + + return nil +} diff --git a/core/services/nodestatusreporter/jobspec/events/emit_test.go b/core/services/nodestatusreporter/jobspec/events/emit_test.go new file mode 100644 index 00000000000..236619620c2 --- /dev/null +++ b/core/services/nodestatusreporter/jobspec/events/emit_test.go @@ -0,0 +1,65 @@ +package events_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/beholder/beholdertest" + + "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/events" +) + +func TestEmitJobSpecEvent_RoundTrip(t *testing.T) { + observer := beholdertest.NewObserver(t) + emitter := beholder.GetEmitter() + + event := &events.JobSpecEvent{ + ExternalJobId: "test-job-id", + Name: "test-job", + JobType: "offchainreporting2", + EmissionTrigger: events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT, + } + + err := events.EmitJobSpecEvent(context.Background(), emitter, event) + require.NoError(t, err) + + msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity) + require.Len(t, msgs, 1) + + msg := msgs[0] + require.Equal(t, "test-job-id", msg.Attrs["partitionkey"]) + require.Equal(t, events.SchemaJobSpec, msg.Attrs["beholder_data_schema"]) + require.Equal(t, events.BeholderDomain, msg.Attrs["beholder_domain"]) + require.NotContains(t, msg.Attrs, "source") + + var decoded events.JobSpecEvent + require.NoError(t, proto.Unmarshal(msg.Body, &decoded)) + require.Equal(t, "test-job-id", decoded.ExternalJobId) + require.Equal(t, "test-job", decoded.Name) + require.Equal(t, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT, decoded.EmissionTrigger) + require.NotEmpty(t, decoded.Timestamp) +} + +func TestEmitJobSpecEvent_SetsTimestampIfEmpty(t *testing.T) { + observer := beholdertest.NewObserver(t) + emitter := beholder.GetEmitter() + + event := &events.JobSpecEvent{ + ExternalJobId: "ts-test", + } + require.Empty(t, event.Timestamp) + + err := events.EmitJobSpecEvent(context.Background(), emitter, event) + require.NoError(t, err) + + msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity) + require.Len(t, msgs, 1) + + var decoded events.JobSpecEvent + require.NoError(t, proto.Unmarshal(msgs[0].Body, &decoded)) + require.NotEmpty(t, decoded.Timestamp) +} diff --git a/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go new file mode 100644 index 00000000000..2f200c555f8 --- /dev/null +++ b/core/services/nodestatusreporter/jobspec/events/job_spec.pb.go @@ -0,0 +1,26 @@ +// Code generated by chainlink-protos import shim. DO NOT EDIT. +// source: job_spec.proto + +package events + +import job_specv1 "github.com/smartcontractkit/chainlink-protos/data-feeds/job_spec/v1" + +type EmissionTrigger = job_specv1.EmissionTrigger + +const ( + EmissionTrigger_EMISSION_TRIGGER_UNSPECIFIED = job_specv1.EmissionTrigger_EMISSION_TRIGGER_UNSPECIFIED + EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT = job_specv1.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT + EmissionTrigger_EMISSION_TRIGGER_CREATE = job_specv1.EmissionTrigger_EMISSION_TRIGGER_CREATE + EmissionTrigger_EMISSION_TRIGGER_DELETE = job_specv1.EmissionTrigger_EMISSION_TRIGGER_DELETE +) + +var ( + EmissionTrigger_name = job_specv1.EmissionTrigger_name + EmissionTrigger_value = job_specv1.EmissionTrigger_value + File_job_spec_proto = job_specv1.File_job_spec_v1_job_spec_event_proto +) + +type JobSpecEvent = job_specv1.JobSpecEvent +type OCR2EVMRelayConfig = job_specv1.OCR2EVMRelayConfig +type OCR2MedianPluginConfig = job_specv1.OCR2MedianPluginConfig +type OCR2OracleSpecInfo = job_specv1.OCR2OracleSpecInfo diff --git a/core/services/nodestatusreporter/jobspec/events/types.go b/core/services/nodestatusreporter/jobspec/events/types.go new file mode 100644 index 00000000000..4718d893c46 --- /dev/null +++ b/core/services/nodestatusreporter/jobspec/events/types.go @@ -0,0 +1,8 @@ +package events + +const ( + ProtoPkg = "job_spec.v1" + JobSpecEventEntity = "JobSpecEvent" + SchemaJobSpec = "/job-spec-events/v1" + BeholderDomain = "data-feeds.job-spec" +) diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go new file mode 100644 index 00000000000..79ec4ffb53c --- /dev/null +++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter.go @@ -0,0 +1,395 @@ +package jobspec + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "slices" + "time" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/services" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + + coreconfig "github.com/smartcontractkit/chainlink/v2/core/config" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/feeds" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/events" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" +) + +const ServiceName = "JobSpecReporter" + +var _ job.Listener = (*Service)(nil) + +// Service polls active jobs and pushes their specs to Beholder, and also emits +// on job create/delete via the job.Listener interface. +type Service struct { + services.Service + eng *services.Engine + + config coreconfig.JobSpecReporter + spawner job.Spawner + feedsORM feeds.ORM + emitter beholder.Emitter + csaPublicKey string + nodeVersion string + hostname string +} + +func NewJobSpecReporter( + config coreconfig.JobSpecReporter, + spawner job.Spawner, + feedsORM feeds.ORM, + emitter beholder.Emitter, + csaPublicKey string, + nodeVersion string, + hostname string, + lggr logger.Logger, +) *Service { + s := &Service{ + config: config, + spawner: spawner, + feedsORM: feedsORM, + emitter: emitter, + csaPublicKey: csaPublicKey, + nodeVersion: nodeVersion, + hostname: hostname, + } + s.Service, s.eng = services.Config{ + Name: ServiceName, + Start: s.start, + }.NewServiceEngine(lggr) + return s +} + +func (s *Service) start(ctx context.Context) error { + if !s.config.Enabled() { + s.eng.Info("Job Spec Reporter Service is disabled") + return nil + } + + s.eng.Info("Starting Job Spec Reporter Service") + s.spawner.RegisterListener(s) + ticker := services.NewTicker(s.config.PollingInterval()) + s.eng.GoTick(ticker, s.pollAllJobs) + + return nil +} + +func (s *Service) HealthReport() map[string]error { + return map[string]error{ServiceName: s.Ready()} +} + +// AfterJobStarted emits a create event when a job starts. +func (s *Service) AfterJobStarted(ctx context.Context, jb job.Job) { + if !s.ShouldEmit(&jb) { + return + } + if err := s.EmitForJob(ctx, jb, events.EmissionTrigger_EMISSION_TRIGGER_CREATE); err != nil { + s.eng.Warnw("Failed to emit job spec telemetry on create", "jobID", jb.ID, "error", err) + } +} + +// AfterJobStopped emits a delete event when a job is removed. +func (s *Service) AfterJobStopped(ctx context.Context, jb job.Job) { + if !s.ShouldEmit(&jb) { + return + } + if err := s.EmitForJob(ctx, jb, events.EmissionTrigger_EMISSION_TRIGGER_DELETE); err != nil { + s.eng.Warnw("Failed to emit job spec telemetry on delete", "jobID", jb.ID, "error", err) + } +} + +// pollAllJobs emits heartbeat telemetry for every active job that passes the emit gate. +func (s *Service) pollAllJobs(ctx context.Context) { + for _, jb := range s.spawner.ActiveJobs() { + if !s.ShouldEmit(&jb) { + continue + } + if err := s.EmitForJob(ctx, jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT); err != nil { + s.eng.Warnw("Failed to emit job spec telemetry", "jobID", jb.ID, "error", err) + } + } +} + +// ShouldEmit reports whether the job passes the config-driven emit gate. +func (s *Service) ShouldEmit(j *job.Job) bool { + if j == nil { + return false + } + if j.Type != job.OffchainReporting2 || j.OCR2OracleSpec == nil { + return false + } + allowed := s.config.EnabledOCR2PluginTypes() + if len(allowed) == 0 { + return false + } + if slices.Contains(allowed, "all") { + return true + } + return slices.Contains(allowed, string(j.OCR2OracleSpec.PluginType)) +} + +// EmitForJob builds and emits a JobSpecEvent for the given job and trigger. +func (s *Service) EmitForJob(ctx context.Context, jb job.Job, trigger events.EmissionTrigger) error { + if jb.Type != job.OffchainReporting2 || jb.OCR2OracleSpec == nil { + return fmt.Errorf("unsupported job type %s", jb.Type) + } + + event, err := s.buildEvent(ctx, jb, trigger) + if err != nil { + return fmt.Errorf("building event: %w", err) + } + + if err := events.EmitJobSpecEvent(ctx, s.emitter, event); err != nil { + return fmt.Errorf("emitting event: %w", err) + } + return nil +} + +// buildEvent converts a job.Job into its protobuf JobSpecEvent representation. +func (s *Service) buildEvent(ctx context.Context, jb job.Job, trigger events.EmissionTrigger) (*events.JobSpecEvent, error) { + event := &events.JobSpecEvent{ + ExternalJobId: jb.ExternalJobID.String(), + Name: jb.Name.ValueOrZero(), + JobType: string(jb.Type), + SchemaVersion: jb.SchemaVersion, + ForwardingAllowed: jb.ForwardingAllowed, + CreatedAt: jb.CreatedAt.Format(time.RFC3339Nano), + CsaPublicKey: s.csaPublicKey, + NodeVersion: s.nodeVersion, + Hostname: s.hostname, + EmissionTrigger: trigger, + Timestamp: time.Now().Format(time.RFC3339Nano), + } + + if jb.GasLimit.Valid { + event.GasLimit = proto.Uint32(jb.GasLimit.Uint32) + } + if jb.StreamID != nil { + sid := *jb.StreamID + event.StreamId = proto.Uint32(sid) + } + + if jb.PipelineSpec != nil { + event.ObservationSource = jb.PipelineSpec.DotDagSource + bridgeNames, err := extractBridgeNames(jb) + if err != nil { + return nil, fmt.Errorf("extracting bridge names: %w", err) + } + event.BridgeNames = bridgeNames + } + + if err := s.populateProposalLifecycle(ctx, jb, event); err != nil { + s.eng.Warnw("Failed to populate proposal lifecycle", "jobID", jb.ID, "error", err) + } + + ocr2Info, err := buildOCR2OracleSpecInfo(jb.OCR2OracleSpec) + if err != nil { + return nil, fmt.Errorf("building OCR2OracleSpecInfo: %w", err) + } + event.Ocr2OracleSpec = ocr2Info + + return event, nil +} + +// populateProposalLifecycle fills in proposal/approval fields for jobs created +// via the Feeds Manager. Jobs not managed by Feeds Manager are a no-op. +func (s *Service) populateProposalLifecycle(ctx context.Context, jb job.Job, event *events.JobSpecEvent) error { + if s.feedsORM == nil || jb.ExternalJobID == uuid.Nil { + return nil + } + + prop, err := s.feedsORM.GetJobProposalByExternalJobID(ctx, jb.ExternalJobID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil + } + return fmt.Errorf("fetching job proposal: %w", err) + } + + spec, err := s.feedsORM.GetApprovedSpec(ctx, prop.ID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil + } + return fmt.Errorf("fetching approved spec: %w", err) + } + + event.FeedsManagerId = prop.FeedsManagerID + event.RemoteUuid = prop.RemoteUUID.String() + event.SpecVersion = spec.Version + event.ProposedAt = spec.CreatedAt.Format(time.RFC3339Nano) + event.ApprovedAt = spec.StatusUpdatedAt.Format(time.RFC3339Nano) + event.AcceptLatencySeconds = spec.StatusUpdatedAt.Sub(spec.CreatedAt).Seconds() + return nil +} + +// extractBridgeNames returns the names of bridge tasks in the top-level pipeline. +// Tasks inside sub-pipelines (e.g. juelsPerFeeCoinSource) are not included. +func extractBridgeNames(jb job.Job) ([]string, error) { + names := extractBridgeNamesFromPipeline(jb.Pipeline) + if len(names) > 0 || jb.PipelineSpec == nil || jb.PipelineSpec.DotDagSource == "" { + return names, nil + } + + p, err := pipeline.Parse(jb.PipelineSpec.DotDagSource) + if err != nil { + return nil, err + } + return extractBridgeNamesFromPipeline(*p), nil +} + +func extractBridgeNamesFromPipeline(p pipeline.Pipeline) []string { + var names []string + for _, task := range p.Tasks { + if task.Type() != pipeline.TaskTypeBridge { + continue + } + bt, ok := task.(*pipeline.BridgeTask) + if !ok { + continue + } + names = append(names, bt.Name) + } + return names +} + +func ocr2ChainID(spec *job.OCR2OracleSpec) string { + if spec == nil { + return "" + } + if spec.ChainID != "" { + return spec.ChainID + } + relayID, err := spec.RelayID() + if err != nil { + return "" + } + return relayID.ChainID +} + +// evmRelayConfig mirrors the EVM relay config JSON so we can surface its fields +// in OCR2EVMRelayConfig without depending on the EVM module. +type evmRelayConfig struct { + FromBlock *uint64 `json:"fromBlock"` + EffectiveTransmitterID *string `json:"effectiveTransmitterID"` + EnableDualTransmission *bool `json:"enableDualTransmission"` + EnableTriggerCapability *bool `json:"enableTriggerCapability"` + LLODonID *uint64 `json:"lloDonID"` + FeedID *string `json:"feedID"` + SendingKeys []string `json:"sendingKeys"` + ProviderType *string `json:"providerType"` +} + +type medianPluginConfig struct { + JuelsPerFeeCoinPipeline *string `json:"juelsPerFeeCoinSource"` +} + +// buildOCR2OracleSpecInfo converts an OCR2OracleSpec into the proto message. +func buildOCR2OracleSpecInfo(spec *job.OCR2OracleSpec) (*events.OCR2OracleSpecInfo, error) { + relayConfigRaw, err := json.Marshal(spec.RelayConfig) + if err != nil { + return nil, fmt.Errorf("marshaling relay config: %w", err) + } + pluginConfigRaw, err := json.Marshal(spec.PluginConfig) + if err != nil { + return nil, fmt.Errorf("marshaling plugin config: %w", err) + } + info := &events.OCR2OracleSpecInfo{ + ContractId: spec.ContractID, + Relay: spec.Relay, + PluginType: string(spec.PluginType), + CaptureEaTelemetry: spec.CaptureEATelemetry, + RelayConfigJson: string(relayConfigRaw), + PluginConfigJson: string(pluginConfigRaw), + } + + if spec.FeedID != nil { + info.FeedId = proto.String(spec.FeedID.Hex()) + } + if spec.TransmitterID.Valid { + info.TransmitterId = proto.String(spec.TransmitterID.String) + } + if spec.OCRKeyBundleID.Valid { + info.OcrKeyBundleId = proto.String(spec.OCRKeyBundleID.String) + } + + if spec.Relay == "evm" { + evmCfg, err := buildEVMRelayConfig(relayConfigRaw, ocr2ChainID(spec), spec.TransmitterID.ValueOrZero()) + if err != nil { + return nil, fmt.Errorf("building EVM relay config: %w", err) + } + info.EvmRelayConfig = evmCfg + } + + if spec.PluginType == commontypes.Median { + medianCfg, err := buildMedianPluginConfig(pluginConfigRaw) + if err != nil { + return nil, fmt.Errorf("building median plugin config: %w", err) + } + info.MedianPluginConfig = medianCfg + } + + return info, nil +} + +// buildEVMRelayConfig decodes the EVM relay config JSON into OCR2EVMRelayConfig. +func buildEVMRelayConfig(relayConfigJSON []byte, chainID, transmitterID string) (*events.OCR2EVMRelayConfig, error) { + var cfg evmRelayConfig + if err := json.Unmarshal(relayConfigJSON, &cfg); err != nil { + return nil, fmt.Errorf("unmarshaling EVM relay config: %w", err) + } + + effectiveTransmitterID := transmitterID + if cfg.EffectiveTransmitterID != nil { + effectiveTransmitterID = *cfg.EffectiveTransmitterID + } + + evmProto := &events.OCR2EVMRelayConfig{ + ChainId: chainID, + EffectiveTransmitterId: effectiveTransmitterID, + SendingKeys: cfg.SendingKeys, + } + if cfg.FromBlock != nil { + evmProto.FromBlock = proto.Uint64(*cfg.FromBlock) + } + if cfg.EnableDualTransmission != nil { + evmProto.EnableDualTransmission = proto.Bool(*cfg.EnableDualTransmission) + } + if cfg.EnableTriggerCapability != nil { + evmProto.EnableTriggerCapability = proto.Bool(*cfg.EnableTriggerCapability) + } + if cfg.LLODonID != nil { + evmProto.LloDonId = proto.Uint64(*cfg.LLODonID) + } + if cfg.FeedID != nil { + evmProto.FeedId = proto.String(*cfg.FeedID) + } + if cfg.ProviderType != nil { + evmProto.ProviderType = proto.String(*cfg.ProviderType) + } + return evmProto, nil +} + +// buildMedianPluginConfig decodes the median plugin config JSON into OCR2MedianPluginConfig. +func buildMedianPluginConfig(pluginConfigJSON []byte) (*events.OCR2MedianPluginConfig, error) { + var cfg medianPluginConfig + if err := json.Unmarshal(pluginConfigJSON, &cfg); err != nil { + return nil, fmt.Errorf("unmarshaling median plugin config: %w", err) + } + + medianProto := &events.OCR2MedianPluginConfig{} + if cfg.JuelsPerFeeCoinPipeline != nil { + medianProto.JuelsPerFeeCoinSource = *cfg.JuelsPerFeeCoinPipeline + } + + return medianProto, nil +} diff --git a/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go new file mode 100644 index 00000000000..4c2c2c3d375 --- /dev/null +++ b/core/services/nodestatusreporter/jobspec/job_spec_reporter_test.go @@ -0,0 +1,392 @@ +package jobspec_test + +import ( + "context" + "database/sql" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "gopkg.in/guregu/null.v4" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/beholder/beholdertest" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/feeds" + feedsmocks "github.com/smartcontractkit/chainlink/v2/core/services/feeds/mocks" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + jobmocks "github.com/smartcontractkit/chainlink/v2/core/services/job/mocks" + "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec" + "github.com/smartcontractkit/chainlink/v2/core/services/nodestatusreporter/jobspec/events" + "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" +) + +// stubConfig implements config.JobSpecReporter for tests. +type stubConfig struct { + enabled bool + pollingInterval time.Duration + enabledOCR2PluginTypes []string +} + +func (s *stubConfig) Enabled() bool { return s.enabled } +func (s *stubConfig) PollingInterval() time.Duration { return s.pollingInterval } +func (s *stubConfig) EnabledOCR2PluginTypes() []string { return s.enabledOCR2PluginTypes } + +func defaultConfig() *stubConfig { + return &stubConfig{ + enabled: true, + pollingInterval: time.Hour, + enabledOCR2PluginTypes: []string{"median"}, + } +} + +func makeMedianJob() job.Job { + return job.Job{ + ID: 1, + ExternalJobID: uuid.New(), + Name: null.StringFrom("test-median-job"), + Type: job.OffchainReporting2, + SchemaVersion: 1, + PipelineSpec: &pipeline.Spec{ + ID: 10, + DotDagSource: `ds1 [type=bridge name="my-bridge"]`, + }, + Pipeline: pipeline.Pipeline{ + Tasks: []pipeline.Task{ + &pipeline.BridgeTask{ + BaseTask: pipeline.NewBaseTask(0, "ds1", nil, nil, 0), + Name: "my-bridge", + }, + }, + }, + OCR2OracleSpec: &job.OCR2OracleSpec{ + ID: 1, + ContractID: "0x1234567890abcdef", + Relay: "evm", + ChainID: "1", + PluginType: commontypes.Median, + TransmitterID: null.StringFrom("0x1111111111111111111111111111111111111111"), + RelayConfig: job.JSONConfig{"chainID": "1"}, + PluginConfig: job.JSONConfig{"juelsPerFeeCoinSource": `ds1 [type=http method=GET url="https://example.com"]`}, + OnchainSigningStrategy: job.JSONConfig{}, + P2PV2Bootstrappers: []string{"12D3KooW@host:6688"}, + ContractConfigConfirmations: 1, + }, + CreatedAt: time.Now(), + } +} + +func makeNonMedianOCR2Job() job.Job { + jb := makeMedianJob() + jb.ID = 2 + jb.ExternalJobID = uuid.New() + jb.Name = null.StringFrom("test-non-median-job") + jb.OCR2OracleSpec = &job.OCR2OracleSpec{ + ID: 2, + ContractID: "0xabcdef1234567890", + Relay: "evm", + ChainID: "1", + PluginType: commontypes.Mercury, + TransmitterID: null.StringFrom("0x2222222222222222222222222222222222222222"), + RelayConfig: job.JSONConfig{"chainID": "1"}, + PluginConfig: job.JSONConfig{}, + OnchainSigningStrategy: job.JSONConfig{}, + } + return jb +} + +func makeNonOCR2Job() job.Job { + return job.Job{ + ID: 3, + ExternalJobID: uuid.New(), + Name: null.StringFrom("test-cron-job"), + Type: job.Cron, + SchemaVersion: 1, + PipelineSpec: &pipeline.Spec{ID: 30, DotDagSource: ""}, + Pipeline: pipeline.Pipeline{}, + CreatedAt: time.Now(), + } +} + +// newTestReporter returns a Service wired to the current global beholder emitter. +// The caller must set up the test emitter via beholdertest.NewObserver(t) first. +func newTestReporter(t *testing.T, cfg *stubConfig, feedsORM feeds.ORM) *jobspec.Service { + t.Helper() + spawner := jobmocks.NewSpawner(t) + return jobspec.NewJobSpecReporter(cfg, spawner, feedsORM, beholder.GetEmitter(), "csa-key", "1.0.0", "test-host", logger.TestLogger(t)) +} + +// newFeedsORMWithoutProposal returns a feeds ORM mock that behaves as if the +// given job was created outside of the Feeds Manager. +func newFeedsORMWithoutProposal(t *testing.T, jb job.Job) *feedsmocks.ORM { + t.Helper() + feedsORM := feedsmocks.NewORM(t) + feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(nil, sql.ErrNoRows).Maybe() + return feedsORM +} + +func requireSingleJobSpecEvent(t *testing.T, observer beholdertest.Observer) *events.JobSpecEvent { + t.Helper() + + msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity) + require.Len(t, msgs, 1) + + ev := new(events.JobSpecEvent) + require.NoError(t, proto.Unmarshal(msgs[0].Body, ev)) + return ev +} + +func TestShouldEmit_DefaultConfig(t *testing.T) { + beholdertest.NewObserver(t) + svc := newTestReporter(t, defaultConfig(), nil) + + median := makeMedianJob() + nonMedian := makeNonMedianOCR2Job() + nonOCR2 := makeNonOCR2Job() + + cases := []struct { + name string + jb *job.Job + want bool + }{ + {"median OCR2 job emits", &median, true}, + {"non-median OCR2 job skipped", &nonMedian, false}, + {"non-OCR2 job skipped", &nonOCR2, false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, svc.ShouldEmit(tc.jb)) + }) + } +} + +func TestShouldEmit_NoOCR2Types(t *testing.T) { + beholdertest.NewObserver(t) + cfg := defaultConfig() + cfg.enabledOCR2PluginTypes = []string{} // empty allowlist = disable all + + svc := newTestReporter(t, cfg, nil) + + median := makeMedianJob() + nonMedian := makeNonMedianOCR2Job() + nonOCR2 := makeNonOCR2Job() + + assert.False(t, svc.ShouldEmit(&median)) + assert.False(t, svc.ShouldEmit(&nonMedian)) + assert.False(t, svc.ShouldEmit(&nonOCR2)) +} + +func TestShouldEmit_AllOCR2Types(t *testing.T) { + beholdertest.NewObserver(t) + cfg := defaultConfig() + cfg.enabledOCR2PluginTypes = []string{"all"} + + svc := newTestReporter(t, cfg, nil) + + median := makeMedianJob() + nonMedian := makeNonMedianOCR2Job() + nonOCR2 := makeNonOCR2Job() + + assert.True(t, svc.ShouldEmit(&median)) + assert.True(t, svc.ShouldEmit(&nonMedian)) + assert.False(t, svc.ShouldEmit(&nonOCR2)) +} + +func TestShouldEmit_NonOCR2Skipped(t *testing.T) { + beholdertest.NewObserver(t) + cfg := defaultConfig() + + svc := newTestReporter(t, cfg, nil) + + median := makeMedianJob() + nonOCR2 := makeNonOCR2Job() + + assert.False(t, svc.ShouldEmit(&nonOCR2)) + assert.True(t, svc.ShouldEmit(&median)) +} + +func TestBuildEvent_MedianJob(t *testing.T) { + observer := beholdertest.NewObserver(t) + + jb := makeMedianJob() + svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb)) + + err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT) + require.NoError(t, err) + + ev := requireSingleJobSpecEvent(t, observer) + assert.Equal(t, jb.ExternalJobID.String(), ev.ExternalJobId) + assert.Equal(t, "test-median-job", ev.Name) + assert.Equal(t, "offchainreporting2", ev.JobType) + assert.Equal(t, jb.CreatedAt.Format(time.RFC3339Nano), ev.CreatedAt) + assert.Equal(t, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT, ev.EmissionTrigger) + assert.Equal(t, "csa-key", ev.CsaPublicKey) + assert.Equal(t, "1.0.0", ev.NodeVersion) + assert.Equal(t, "test-host", ev.Hostname) + assert.Equal(t, []string{"my-bridge"}, ev.BridgeNames) + require.NotNil(t, ev.Ocr2OracleSpec) + assert.Equal(t, "0x1234567890abcdef", ev.Ocr2OracleSpec.GetContractId()) + assert.Equal(t, "evm", ev.Ocr2OracleSpec.Relay) + assert.Equal(t, "median", ev.Ocr2OracleSpec.PluginType) + require.NotNil(t, ev.Ocr2OracleSpec.MedianPluginConfig) + assert.NotEmpty(t, ev.Ocr2OracleSpec.MedianPluginConfig.GetJuelsPerFeeCoinSource()) + require.NotNil(t, ev.Ocr2OracleSpec.EvmRelayConfig) + assert.Equal(t, "1", ev.Ocr2OracleSpec.EvmRelayConfig.GetChainId()) + assert.Equal(t, "0x1111111111111111111111111111111111111111", ev.Ocr2OracleSpec.EvmRelayConfig.GetEffectiveTransmitterId()) +} + +func TestBuildEvent_MedianJobBridgeNamesFromPipelineSpec(t *testing.T) { + observer := beholdertest.NewObserver(t) + + jb := makeMedianJob() + jb.Pipeline = pipeline.Pipeline{} + svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb)) + + err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT) + require.NoError(t, err) + + ev := requireSingleJobSpecEvent(t, observer) + assert.Equal(t, []string{"my-bridge"}, ev.BridgeNames) +} + +func TestBuildEvent_MedianJobNumericRelayConfigChainID(t *testing.T) { + observer := beholdertest.NewObserver(t) + + jb := makeMedianJob() + jb.OCR2OracleSpec.ChainID = "" + jb.OCR2OracleSpec.RelayConfig["chainID"] = int64(11155111) + svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb)) + + err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT) + require.NoError(t, err) + + ev := requireSingleJobSpecEvent(t, observer) + require.NotNil(t, ev.Ocr2OracleSpec) + require.NotNil(t, ev.Ocr2OracleSpec.EvmRelayConfig) + assert.Equal(t, "11155111", ev.Ocr2OracleSpec.EvmRelayConfig.GetChainId()) +} + +func TestBuildEvent_EVMRelayConfigEmitsExplicitFalseBooleans(t *testing.T) { + observer := beholdertest.NewObserver(t) + + jb := makeMedianJob() + jb.OCR2OracleSpec.RelayConfig["enableDualTransmission"] = false + jb.OCR2OracleSpec.RelayConfig["enableTriggerCapability"] = false + svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb)) + + err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT) + require.NoError(t, err) + + ev := requireSingleJobSpecEvent(t, observer) + require.NotNil(t, ev.Ocr2OracleSpec) + require.NotNil(t, ev.Ocr2OracleSpec.EvmRelayConfig) + assert.False(t, ev.Ocr2OracleSpec.EvmRelayConfig.GetEnableDualTransmission()) + assert.False(t, ev.Ocr2OracleSpec.EvmRelayConfig.GetEnableTriggerCapability()) + assert.NotNil(t, ev.Ocr2OracleSpec.EvmRelayConfig.EnableDualTransmission) + assert.NotNil(t, ev.Ocr2OracleSpec.EvmRelayConfig.EnableTriggerCapability) +} + +func TestBuildEvent_NonMedianOCR2Job(t *testing.T) { + observer := beholdertest.NewObserver(t) + + jb := makeNonMedianOCR2Job() + svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb)) + + err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_CREATE) + require.NoError(t, err) + + ev := requireSingleJobSpecEvent(t, observer) + require.NotNil(t, ev.Ocr2OracleSpec) + assert.Equal(t, "mercury", ev.Ocr2OracleSpec.PluginType) + assert.Nil(t, ev.Ocr2OracleSpec.MedianPluginConfig) + assert.NotEmpty(t, ev.Ocr2OracleSpec.RelayConfigJson) +} + +func TestBuildEvent_NonOCR2Job(t *testing.T) { + observer := beholdertest.NewObserver(t) + + svc := newTestReporter(t, defaultConfig(), nil) + + jb := makeNonOCR2Job() + err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT) + require.ErrorContains(t, err, "unsupported job type") + + msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity) + require.Empty(t, msgs) +} + +func TestAfterJobStarted_EmitsCreate(t *testing.T) { + observer := beholdertest.NewObserver(t) + + jb := makeMedianJob() + svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb)) + svc.AfterJobStarted(context.Background(), jb) + + ev := requireSingleJobSpecEvent(t, observer) + assert.Equal(t, events.EmissionTrigger_EMISSION_TRIGGER_CREATE, ev.EmissionTrigger) +} + +func TestAfterJobStopped_EmitsDelete(t *testing.T) { + observer := beholdertest.NewObserver(t) + + jb := makeMedianJob() + svc := newTestReporter(t, defaultConfig(), newFeedsORMWithoutProposal(t, jb)) + svc.AfterJobStopped(context.Background(), jb) + + ev := requireSingleJobSpecEvent(t, observer) + assert.Equal(t, events.EmissionTrigger_EMISSION_TRIGGER_DELETE, ev.EmissionTrigger) +} + +func TestAfterJobStarted_SkippedWhenGateFails(t *testing.T) { + observer := beholdertest.NewObserver(t) + + // default config only allows median, so a non-OCR2 job should be skipped + svc := newTestReporter(t, defaultConfig(), nil) + svc.AfterJobStarted(context.Background(), makeNonOCR2Job()) + + msgs := observer.Messages(t, "beholder_entity", events.ProtoPkg+"."+events.JobSpecEventEntity) + require.Empty(t, msgs) +} + +func TestBuildEvent_ProposalLifecycle(t *testing.T) { + observer := beholdertest.NewObserver(t) + feedsORM := feedsmocks.NewORM(t) + + jb := makeMedianJob() + proposedAt := time.Now().Add(-5 * time.Minute) + approvedAt := time.Now().Add(-2 * time.Minute) + + prop := &feeds.JobProposal{ + ID: 100, + FeedsManagerID: 7, + RemoteUUID: uuid.New(), + } + spec := &feeds.JobProposalSpec{ + ID: 200, + Version: 3, + CreatedAt: proposedAt, + StatusUpdatedAt: approvedAt, + } + + feedsORM.On("GetJobProposalByExternalJobID", mock.Anything, jb.ExternalJobID).Return(prop, nil) + feedsORM.On("GetApprovedSpec", mock.Anything, prop.ID).Return(spec, nil) + + svc := newTestReporter(t, defaultConfig(), feedsORM) + err := svc.EmitForJob(context.Background(), jb, events.EmissionTrigger_EMISSION_TRIGGER_HEARTBEAT) + require.NoError(t, err) + + ev := requireSingleJobSpecEvent(t, observer) + assert.Equal(t, int64(7), ev.FeedsManagerId) + assert.Equal(t, prop.RemoteUUID.String(), ev.RemoteUuid) + assert.Equal(t, int32(3), ev.SpecVersion) + assert.Equal(t, proposedAt.Format(time.RFC3339Nano), ev.ProposedAt) + assert.Equal(t, approvedAt.Format(time.RFC3339Nano), ev.ApprovedAt) + assert.InDelta(t, approvedAt.Sub(proposedAt).Seconds(), ev.AcceptLatencySeconds, 1.0) +} diff --git a/core/services/pipeline/bridge_required_paths_test.go b/core/services/pipeline/bridge_required_paths_test.go new file mode 100644 index 00000000000..96a6fffa58b --- /dev/null +++ b/core/services/pipeline/bridge_required_paths_test.go @@ -0,0 +1,342 @@ +package pipeline + +import ( + "testing" + + "github.com/buger/jsonparser" + "github.com/stretchr/testify/require" +) + +// TestingSetBridgeRequiredJSONPaths sets required JSON paths on a bridge task +// for tests in external test packages (e.g. pipeline_test). +func TestingSetBridgeRequiredJSONPaths(t *BridgeTask, paths [][]string) { + t.requiredJSONPaths = paths +} + +func TestRequiredJSONPathsFromBridge_DirectEdge(t *testing.T) { + t.Parallel() + + dot := ` +b [type=bridge name=testbridge]; +p [type=jsonparse path="data,result"]; +b -> p; +` + pl, err := Parse(dot) + require.NoError(t, err) + + var bt *BridgeTask + for _, tk := range pl.Tasks { + if tk.DotID() == "b" { + bt = tk.(*BridgeTask) + break + } + } + require.NotNil(t, bt) + + paths := bt.getRequiredJSONPaths() + require.Equal(t, [][]string{{"data", "result"}}, paths) +} + +func TestRequiredJSONPathsFromBridge_DataVarRoot(t *testing.T) { + t.Parallel() + + dot := ` +b [type=bridge name=testbridge]; +p [type=jsonparse path="a,b" data="$(b)"]; +` + pl, err := Parse(dot) + require.NoError(t, err) + + var bt *BridgeTask + for _, tk := range pl.Tasks { + if tk.DotID() == "b" { + bt = tk.(*BridgeTask) + break + } + } + require.NotNil(t, bt) + + paths := bt.getRequiredJSONPaths() + require.Equal(t, [][]string{{"a", "b"}}, paths) +} + +func TestRequiredJSONPathsFromBridge_CustomSeparator(t *testing.T) { + t.Parallel() + + dot := ` +b [type=bridge name=testbridge]; +p [type=jsonparse path="a|b|c" separator="|"]; +b -> p; +` + pl, err := Parse(dot) + require.NoError(t, err) + + var bt *BridgeTask + for _, tk := range pl.Tasks { + if tk.DotID() == "b" { + bt = tk.(*BridgeTask) + break + } + } + require.NotNil(t, bt) + + paths := bt.getRequiredJSONPaths() + require.Equal(t, [][]string{{"a", "b", "c"}}, paths) +} + +func TestRequiredJSONPathsFromBridge_SkipsLax(t *testing.T) { + t.Parallel() + + dot := ` +b [type=bridge name=testbridge]; +p [type=jsonparse path="data,result" lax="true"]; +b -> p; +` + pl, err := Parse(dot) + require.NoError(t, err) + + var bt *BridgeTask + for _, tk := range pl.Tasks { + if tk.DotID() == "b" { + bt = tk.(*BridgeTask) + break + } + } + require.NotNil(t, bt) + + paths := bt.getRequiredJSONPaths() + require.Nil(t, paths) +} + +func TestRequiredJSONPathsFromBridge_SkipsDynamicPath(t *testing.T) { + t.Parallel() + + dot := ` +b [type=bridge name=testbridge]; +p [type=jsonparse path="$(jobRun.x)"]; +b -> p; +` + pl, err := Parse(dot) + require.NoError(t, err) + + var bt *BridgeTask + for _, tk := range pl.Tasks { + if tk.DotID() == "b" { + bt = tk.(*BridgeTask) + break + } + } + require.NotNil(t, bt) + + paths := bt.getRequiredJSONPaths() + require.Nil(t, paths) +} + +func TestRequiredJSONPathsFromBridge_LowestOutputIndexWins(t *testing.T) { + t.Parallel() + + dot := ` +b0 [type=bridge name=a index=0]; +b1 [type=bridge name=b index=1]; +p [type=jsonparse path="only,b0"]; +b0 -> p; +b1 -> p; +` + pl, err := Parse(dot) + require.NoError(t, err) + + var b0, b1 *BridgeTask + for _, tk := range pl.Tasks { + switch tk.DotID() { + case "b0": + b0 = tk.(*BridgeTask) + case "b1": + b1 = tk.(*BridgeTask) + } + } + require.NotNil(t, b0) + require.NotNil(t, b1) + + require.Equal(t, [][]string{{"only", "b0"}}, b0.getRequiredJSONPaths()) + require.Nil(t, b1.getRequiredJSONPaths()) +} + +func TestRequiredJSONPathsFromBridge_DedupesPaths(t *testing.T) { + t.Parallel() + + dot := ` +b [type=bridge name=testbridge]; +p1 [type=jsonparse path="data,result"]; +p2 [type=jsonparse path="data,result"]; +b -> p1; +b -> p2; +` + pl, err := Parse(dot) + require.NoError(t, err) + + var bt *BridgeTask + for _, tk := range pl.Tasks { + if tk.DotID() == "b" { + bt = tk.(*BridgeTask) + break + } + } + require.NotNil(t, bt) + + paths := bt.getRequiredJSONPaths() + require.Equal(t, [][]string{{"data", "result"}}, paths) +} + +func TestRequiredJSONPathsFromBridge_EmptyPathSkipped(t *testing.T) { + t.Parallel() + + dot := ` +b [type=bridge name=testbridge]; +p [type=jsonparse path="" ]; +b -> p; +` + pl, err := Parse(dot) + require.NoError(t, err) + + var bt *BridgeTask + for _, tk := range pl.Tasks { + if tk.DotID() == "b" { + bt = tk.(*BridgeTask) + break + } + } + require.NotNil(t, bt) + + paths := bt.getRequiredJSONPaths() + require.Nil(t, paths) +} + +func TestJSONDecodeValidateRequiredPaths(t *testing.T) { + t.Parallel() + + t.Run("empty_paths_noop", func(t *testing.T) { + t.Parallel() + require.NoError(t, jsonDecodeValidateRequiredPaths([]byte(`{}`), nil)) + require.NoError(t, jsonDecodeValidateRequiredPaths([]byte(`{}`), [][]string{})) + }) + + t.Run("json_number_and_string_ok", func(t *testing.T) { + t.Parallel() + require.NoError(t, jsonDecodeValidateRequiredPaths( + []byte(`{"data":{"result":42}}`), [][]string{{"data", "result"}})) + require.NoError(t, jsonDecodeValidateRequiredPaths( + []byte(`{"data":{"result":"123.45"}}`), [][]string{{"data", "result"}})) + }) + + t.Run("large_finite_decimal_json_number", func(t *testing.T) { + t.Parallel() + // Decimal accepts magnitudes float64 cannot represent. + body := []byte(`{"data":{"result":1e309}}`) + require.NoError(t, jsonDecodeValidateRequiredPaths(body, [][]string{{"data", "result"}})) + }) + + t.Run("missing_path", func(t *testing.T) { + t.Parallel() + body := []byte(`{"data":{"result":1}}`) + err := jsonDecodeValidateRequiredPaths(body, [][]string{{"data", "missing"}}) + require.Error(t, err) + require.ErrorIs(t, err, jsonparser.KeyPathNotFoundError) + }) + + t.Run("null_value", func(t *testing.T) { + t.Parallel() + body := []byte(`{"data":{"result":null}}`) + err := jsonDecodeValidateRequiredPaths(body, [][]string{{"data", "result"}}) + require.Error(t, err) + require.Contains(t, err.Error(), "is null") + }) + + t.Run("scientific_notation_rejected_after_decimal_fails", func(t *testing.T) { + t.Parallel() + // Decimal rejects huge exponent; must not be accepted via a hex fallback (digits+e are valid hex). + body := []byte(`{"data":{"result":1e10000000000}}`) + err := jsonDecodeValidateRequiredPaths(body, [][]string{{"data", "result"}}) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid value") + }) + + t.Run("nan_string_rejected", func(t *testing.T) { + t.Parallel() + err := jsonDecodeValidateRequiredPaths( + []byte(`{"data":{"result":"NaN"}}`), [][]string{{"data", "result"}}) + require.Error(t, err) + require.Contains(t, err.Error(), "NaN") + + require.Error(t, jsonDecodeValidateRequiredPaths( + []byte(`{"data":{"result":"nan"}}`), [][]string{{"data", "result"}})) + }) + + t.Run("hex_literals", func(t *testing.T) { + t.Parallel() + require.NoError(t, jsonDecodeValidateRequiredPaths( + []byte(`{"data":{"result":"0xff"}}`), [][]string{{"data", "result"}})) + require.NoError(t, jsonDecodeValidateRequiredPaths( + []byte(`{"data":{"result":"0XAbCd"}}`), [][]string{{"data", "result"}})) + require.NoError(t, jsonDecodeValidateRequiredPaths( + []byte(`{"data":{"result":"-0x10"}}`), [][]string{{"data", "result"}})) + }) + + t.Run("large_hex_integer", func(t *testing.T) { + t.Parallel() + body := []byte(`{"data":{"result":"0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}`) + require.NoError(t, jsonDecodeValidateRequiredPaths(body, [][]string{{"data", "result"}})) + }) + + t.Run("invalid_hex", func(t *testing.T) { + t.Parallel() + err := jsonDecodeValidateRequiredPaths( + []byte(`{"data":{"result":"0x"}}`), [][]string{{"data", "result"}}) + require.Error(t, err) + err = jsonDecodeValidateRequiredPaths( + []byte(`{"data":{"result":"0xGG"}}`), [][]string{{"data", "result"}}) + require.Error(t, err) + }) + + t.Run("non_numeric_string_rejected", func(t *testing.T) { + t.Parallel() + err := jsonDecodeValidateRequiredPaths( + []byte(`{"data":{"result":"ok"}}`), [][]string{{"data", "result"}}) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid value") + }) + + t.Run("multiple_paths", func(t *testing.T) { + t.Parallel() + body := []byte(`{"a":1,"b":"0x2a"}`) + require.NoError(t, jsonDecodeValidateRequiredPaths(body, [][]string{{"a"}, {"b"}})) + }) + + t.Run("empty_path_segment_skipped", func(t *testing.T) { + t.Parallel() + // jsonDecodeValidateRequiredPaths ignores empty segment lists (same as no required paths). + require.NoError(t, jsonDecodeValidateRequiredPaths( + []byte(`{}`), [][]string{{}})) + }) +} + +func TestRequiredJSONPathsFromBridge_NilTask(t *testing.T) { + t.Parallel() + require.Nil(t, ((*BridgeTask)(nil)).getRequiredJSONPaths()) +} + +func TestParseBridgeTask_CheckRequired(t *testing.T) { + t.Parallel() + + dot := `b [type=bridge name=testbridge checkRequired=true];` + pl, err := Parse(dot) + require.NoError(t, err) + var bt *BridgeTask + for _, tk := range pl.Tasks { + if tk.DotID() == "b" { + bt = tk.(*BridgeTask) + break + } + } + require.NotNil(t, bt) + require.Equal(t, "true", bt.CheckRequired) +} diff --git a/core/services/pipeline/jsonpath_traverse.go b/core/services/pipeline/jsonpath_traverse.go new file mode 100644 index 00000000000..1db6672f5ac --- /dev/null +++ b/core/services/pipeline/jsonpath_traverse.go @@ -0,0 +1,73 @@ +package pipeline + +import ( + "math/big" + "strings" + + "github.com/pkg/errors" +) + +// traverseJSONPath walks decoded JSON (map/slice tree) along path the same way +// JSONParseTask does. When lax is false, a missing segment returns ErrKeypathNotFound. +func traverseJSONPath(decoded any, path []string, lax bool) (any, error) { + for _, part := range path { + switch d := decoded.(type) { + case map[string]any: + var exists bool + decoded, exists = d[part] + if !exists && lax { + decoded = nil + break + } else if !exists { + return nil, errJSONPathNotFound(path) + } + + case []any: + next, laxMiss, err := jsonPathArrayStep(d, part, path, lax) + if err != nil { + return nil, err + } + if laxMiss { + decoded = nil + break + } + decoded = next + + default: + return nil, errJSONPathNotFound(path) + } + } + return decoded, nil +} + +func errJSONPathNotFound(path []string) error { + return errors.Wrapf(ErrKeypathNotFound, `could not resolve path ["%v"]`, strings.Join(path, `","`)) +} + +// jsonPathArrayStep resolves one path segment against a JSON array. laxMiss is true when lax +// mode treats the segment as missing (decoded becomes nil). err is set for invalid index syntax +// or strict resolution failure. +func jsonPathArrayStep(d []any, part string, path []string, lax bool) (next any, laxMiss bool, err error) { + bi, ok := big.NewInt(0).SetString(part, 10) + if !ok { + return nil, false, errors.Wrapf(ErrKeypathNotFound, "JSONParse task error: %v is not a valid array index", part) + } + if !bi.IsInt64() { + if lax { + return nil, true, nil + } + return nil, false, errJSONPathNotFound(path) + } + + index := int(bi.Int64()) + if index < 0 { + index = len(d) + index + } + if index < 0 || index >= len(d) { + if lax { + return nil, true, nil + } + return nil, false, errJSONPathNotFound(path) + } + return d[index], false, nil +} diff --git a/core/services/pipeline/models.go b/core/services/pipeline/models.go index 66f5a529a3f..9d970feab89 100644 --- a/core/services/pipeline/models.go +++ b/core/services/pipeline/models.go @@ -19,7 +19,9 @@ import ( ) type Spec struct { - ID int32 + ID int32 + // DotDagSource is the pipeline graph in DOT notation. Bridge tasks may set + // checkRequired=true on a node to validate required JSON paths for cache fallback. DotDagSource string `json:"dotDagSource"` CreatedAt time.Time `json:"-"` MaxTaskDuration sqlutil.Interval `json:"-"` diff --git a/core/services/pipeline/runner.go b/core/services/pipeline/runner.go index 04e86072d70..72e535687b9 100644 --- a/core/services/pipeline/runner.go +++ b/core/services/pipeline/runner.go @@ -339,15 +339,17 @@ func (r *runner) InitializePipeline(spec Spec) (pipeline *Pipeline, err error) { task.(*HTTPTask).httpClient = r.httpClient task.(*HTTPTask).unrestrictedHTTPClient = r.unrestrictedHTTPClient case TaskTypeBridge: - task.(*BridgeTask).config = r.config - task.(*BridgeTask).bridgeConfig = r.bridgeConfig + bt := task.(*BridgeTask) + bt.config = r.config + bt.bridgeConfig = r.bridgeConfig // orm added to BridgeTask - task.(*BridgeTask).orm = r.btORM - task.(*BridgeTask).specId = spec.ID + bt.orm = r.btORM + bt.specId = spec.ID // URL is "safe" because it comes from the node's own database. We // must use the unrestrictedHTTPClient because some node operators // may run external adapters on their own hardware - task.(*BridgeTask).httpClient = r.unrestrictedHTTPClient + bt.httpClient = r.unrestrictedHTTPClient + bt.requiredJSONPaths = bt.getRequiredJSONPaths() case TaskTypeETHCall: task.(*ETHCallTask).legacyChains = r.legacyEVMChains task.(*ETHCallTask).config = r.config diff --git a/core/services/pipeline/task.bridge.go b/core/services/pipeline/task.bridge.go index 8d9bd82fb26..d19e0993a9d 100644 --- a/core/services/pipeline/task.bridge.go +++ b/core/services/pipeline/task.bridge.go @@ -5,15 +5,22 @@ import ( "database/sql" stderrors "errors" "maps" + "math" + "math/big" "net/http" "net/url" "path" + "regexp" + "strconv" + "strings" "time" + "github.com/buger/jsonparser" "github.com/goccy/go-json" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/shopspring/decimal" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/v2/core/bridges" @@ -73,12 +80,31 @@ type BridgeTask struct { Async string `json:"async"` CacheTTL string `json:"cacheTTL"` Headers string `json:"headers"` + // CheckRequired when "true" enables validation that the HTTP response JSON + // contains paths required by strict downstream jsonparse tasks (see + // requiredJSONPaths). When empty or "false", that check is skipped. + CheckRequired string `json:"checkRequired"` specId int32 orm bridges.ORM config Config bridgeConfig BridgeConfig httpClient *http.Client + + // requiredJSONPaths is populated in runner.InitializePipeline from strict + // downstream jsonparse tasks. When CheckRequired is true and cacheTTL is set, + // validation uses these paths to fall back to cache when the live response + // omits required keys. + requiredJSONPaths [][]string +} + +// bridgeHTTPOutcome holds response state after the HTTP round-trip through EA JSON status, +// optional required-path validation, and optional cache fallback. +type bridgeHTTPOutcome struct { + body []byte + statusCode int + err error + cachedResponse bool } type BridgeTelemetry struct { @@ -115,6 +141,7 @@ func (t *BridgeTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, inp includeInputAtKey StringParam cacheTTL Uint64Param reqHeaders StringSliceParam + checkRequired BoolParam ) err = stderrors.Join( errors.Wrap(ResolveParam(&name, From(NonemptyString(t.Name))), "name"), @@ -122,6 +149,7 @@ func (t *BridgeTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, inp errors.Wrap(ResolveParam(&includeInputAtKey, From(t.IncludeInputAtKey)), "includeInputAtKey"), errors.Wrap(ResolveParam(&cacheTTL, From(ValidDurationInSeconds(t.CacheTTL), t.bridgeConfig.BridgeCacheTTL().Seconds())), "cacheTTL"), errors.Wrap(ResolveParam(&reqHeaders, From(NonemptyString(t.Headers), "[]")), "reqHeaders"), + errors.Wrap(ResolveParam(&checkRequired, From(NonemptyString(t.CheckRequired), false)), "checkRequired"), ) if err != nil { return Result{Error: err}, runInfo @@ -139,40 +167,7 @@ func (t *BridgeTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, inp return Result{Error: err}, runInfo } - var metaMap MapParam - - meta, _ := vars.Get("jobRun.meta") - switch v := meta.(type) { - case map[string]any: - metaMap = MapParam(v) - case nil: - default: - lggr.Warnw(`"meta" field on task run is malformed, discarding`, - "task", t.DotID(), - "meta", meta, - ) - } - - requestData = withRunInfo(requestData, metaMap) - if t.IncludeInputAtKey != "" { - if len(inputValues) > 0 { - requestData[string(includeInputAtKey)] = inputValues[0] - } - } - - if t.Async == "true" { - responseURL := t.bridgeConfig.BridgeResponseURL() - if responseURL != nil && *responseURL != *zeroURL { - responseURL.Path = path.Join(responseURL.Path, "/v2/resume/", t.uuid.String()) - } - var s string - if responseURL != nil { - s = responseURL.String() - } - requestData["responseURL"] = s - } - - requestDataJSON, err := json.Marshal(requestData) + requestDataJSON, err := t.finalizeAndMarshalBridgeRequestData(lggr, vars, inputValues, &requestData, includeInputAtKey) if err != nil { return Result{Error: err}, runInfo } @@ -190,7 +185,16 @@ func (t *BridgeTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, inp promBridgeLatency.WithLabelValues(t.Name, statusCodeGroup(statusCode)).Set(elapsed.Seconds()) promBridgeLatencyHist.WithLabelValues(t.Name, statusCodeGroup(statusCode)).Observe(float64(elapsed.Milliseconds())) + out := bridgeHTTPOutcome{ + body: responseBytes, + statusCode: statusCode, + err: err, + cachedResponse: false, + } + defer func() { + // Runs when Run returns; reads the final err, responseBytes, statusCode, and cachedResponse + // after EA JSON handling, required-path validation, and optional cache fallback. telemetryCh := GetTelemetryCh(ctx) if telemetryCh != nil { bt := &BridgeTelemetry{ @@ -219,47 +223,20 @@ func (t *BridgeTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, inp } }() - // check for external adapter response object status - if code, ok := eautils.BestEffortExtractEAStatus(responseBytes); ok { - statusCode = code - } - - if err != nil || statusCode != http.StatusOK { - if adapterErr := eautils.BestEffortExtractEAError(responseBytes); adapterErr != nil { - err = adapterErr - } - - promBridgeErrors.WithLabelValues(t.Name).Inc() - if cacheTTL == 0 { - lggr.Debugw("Bridge task: request failed", - "response", string(responseBytes), - "url", url.String(), - "status_code", statusCode, - "error", err, - ) - return Result{Error: err}, RunInfo{IsRetryable: isRetryableHTTPError(statusCode, err)} - } + out.statusCode = eaJSONResponseStatus(out.body, out.statusCode) + liveOK := out.err == nil && out.statusCode == http.StatusOK + out.err = t.maybeValidateRequiredJSONPaths(out.err, liveOK, cacheTTL, checkRequired, out.body) - var cacheErr error - responseBytes, cacheErr = t.orm.GetCachedResponse(overtimeCtx, t.dotID, t.specId, time.Duration(cacheTTL)*time.Second) - if cacheErr != nil { - promBridgeCacheErrors.WithLabelValues(t.Name).Inc() - if !errors.Is(cacheErr, sql.ErrNoRows) { - lggr.Warnw("Bridge task: cache fallback failed", - "err", cacheErr.Error(), - "url", url.String(), - ) - } - return Result{Error: err}, RunInfo{IsRetryable: isRetryableHTTPError(statusCode, err)} - } - promBridgeCacheHits.WithLabelValues(t.Name).Inc() - lggr.Debugw("Bridge task: request failed, falling back to cache", - "response", string(responseBytes), - "url", url.String(), - ) - cachedResponse = true + out, earlyResult, earlyRunInfo := t.resolveFailureOrCache(overtimeCtx, lggr, url, out, cacheTTL) + if earlyResult != nil { + return *earlyResult, *earlyRunInfo } + responseBytes = out.body + statusCode = out.statusCode + err = out.err + cachedResponse = out.cachedResponse + if t.Async == "true" { // Look for a `pending` flag. This check is case-insensitive because http.Header normalizes header names if _, ok := headers["X-Chainlink-Pending"]; ok { @@ -299,6 +276,122 @@ func (t *BridgeTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, inp return result, runInfo } +// finalizeAndMarshalBridgeRequestData merges job meta, upstream inputs, and async resume URL into requestData, +// writes the merged map back through requestData for use by makeHTTPRequest, and returns the JSON body for logging +// and telemetry. +func (t *BridgeTask) finalizeAndMarshalBridgeRequestData(lggr logger.Logger, vars Vars, inputValues []any, requestData *MapParam, includeInputAtKey StringParam) ([]byte, error) { + var metaMap MapParam + + meta, _ := vars.Get("jobRun.meta") + switch v := meta.(type) { + case map[string]any: + metaMap = MapParam(v) + case nil: + default: + lggr.Warnw(`"meta" field on task run is malformed, discarding`, + "task", t.DotID(), + "meta", meta, + ) + } + + merged := withRunInfo(*requestData, metaMap) + if t.IncludeInputAtKey != "" { + if len(inputValues) > 0 { + merged[string(includeInputAtKey)] = inputValues[0] + } + } + + if t.Async == "true" { + responseURL := t.bridgeConfig.BridgeResponseURL() + if responseURL != nil && *responseURL != *zeroURL { + responseURL.Path = path.Join(responseURL.Path, "/v2/resume/", t.uuid.String()) + } + var s string + if responseURL != nil { + s = responseURL.String() + } + merged["responseURL"] = s + } + + *requestData = merged + return json.Marshal(merged) +} + +// eaJSONResponseStatus returns the external-adapter status from the response body when present, otherwise the +// HTTP status from the round-trip. +func eaJSONResponseStatus(body []byte, httpStatus int) int { + if code, ok := eautils.BestEffortExtractEAStatus(body); ok { + return code + } + return httpStatus +} + +// maybeValidateRequiredJSONPaths runs jsonDecodeValidateRequiredPaths when the live call succeeded (HTTP 200, no +// transport error), checkRequired is enabled, cache is enabled, and the pipeline registered required paths. A +// failed check sets err so the caller can fall back to the bridge cache when configured. +func (t *BridgeTask) maybeValidateRequiredJSONPaths(err error, liveOK bool, cacheTTL Uint64Param, checkRequired BoolParam, responseBytes []byte) error { + if err != nil || !liveOK || cacheTTL == 0 || !bool(checkRequired) || len(t.requiredJSONPaths) == 0 { + return err + } + if verr := jsonDecodeValidateRequiredPaths(responseBytes, t.requiredJSONPaths); verr != nil { + return errors.Wrap(verr, "bridge response failed required JSON path check for downstream jsonparse") + } + return err +} + +// resolveFailureOrCache handles a non-success HTTP outcome: it prefers the EA error from the body over the +// transport error, increments error metrics, then either returns immediately (no cache TTL or cache miss) or +// replaces the response body from the bridge cache. Non-nil early Result and RunInfo mean the caller must return +// without continuing the success path. +func (t *BridgeTask) resolveFailureOrCache( + ctx context.Context, + lggr logger.Logger, + url URLParam, + out bridgeHTTPOutcome, + cacheTTL Uint64Param, +) (bridgeHTTPOutcome, *Result, *RunInfo) { + if out.err == nil && out.statusCode == http.StatusOK { + return out, nil, nil + } + if adapterErr := eautils.BestEffortExtractEAError(out.body); adapterErr != nil { + out.err = adapterErr + } + + promBridgeErrors.WithLabelValues(t.Name).Inc() + if cacheTTL == 0 { + lggr.Debugw("Bridge task: request failed", + "response", string(out.body), + "url", url.String(), + "status_code", out.statusCode, + "error", out.err, + ) + retry := RunInfo{IsRetryable: isRetryableHTTPError(out.statusCode, out.err)} + return out, &Result{Error: out.err}, &retry + } + + //nolint:gosec // disable G115 + cachedBytes, cacheErr := t.orm.GetCachedResponse(ctx, t.dotID, t.specId, time.Duration(cacheTTL)*time.Second) + if cacheErr != nil { + promBridgeCacheErrors.WithLabelValues(t.Name).Inc() + if !errors.Is(cacheErr, sql.ErrNoRows) { + lggr.Warnw("Bridge task: cache fallback failed", + "err", cacheErr.Error(), + "url", url.String(), + ) + } + retry := RunInfo{IsRetryable: isRetryableHTTPError(out.statusCode, out.err)} + return out, &Result{Error: out.err}, &retry + } + promBridgeCacheHits.WithLabelValues(t.Name).Inc() + lggr.Debugw("Bridge task: request failed, falling back to cache", + "response", string(cachedBytes), + "url", url.String(), + ) + out.body = cachedBytes + out.cachedResponse = true + return out, nil, nil +} + func (bt *BridgeTelemetry) resolveStreamID(t *BridgeTask, vars Vars, lggr logger.Logger) { if t.StreamID.Valid { bt.StreamID = &t.StreamID.Uint32 @@ -331,3 +424,158 @@ func withRunInfo(request MapParam, meta MapParam) MapParam { } return output } + +// getRequiredJSONPaths returns JSON path segments (split the same way as jsonparse) that strict +// downstream jsonparse tasks require when they read this bridge task's string output directly. +// +// Limitations: does not follow merge/median/etc.; skips lax jsonparse, dynamic path/data +// containing "$(", data="$(bridge.field)", or jsonparse whose data input is not this bridge +// (by lowest output index or explicit $(bridgeID)). +func (t *BridgeTask) getRequiredJSONPaths() [][]string { + if t == nil { + return nil + } + bridgeID := t.ID() + bridgeDot := t.DotID() + + seen := make(map[string]struct{}) + var out [][]string + + for _, d := range t.GetDescendantTasks() { + jp, ok := d.(*JSONParseTask) + if !ok { + continue + } + if jsonParseLaxStatic(jp) { + continue + } + if strings.Contains(jp.Path, "$(") { + continue + } + dataTrim := strings.TrimSpace(jp.Data) + if strings.Contains(dataTrim, "$(") { + m := bridgeDataRootVarRegexp.FindStringSubmatch(dataTrim) + if len(m) != 2 || m[1] != bridgeDot { + continue + } + } + if !jsonParseDataReferencesBridge(jp, bridgeID, bridgeDot) { + continue + } + segs, ok := jsonParseStaticPathSegments(jp) + if !ok { + continue + } + key := strings.Join(segs, "\x00") + if _, dup := seen[key]; dup { + continue + } + seen[key] = struct{}{} + out = append(out, segs) + } + return out +} + +// bridgeDataRootVarRegexp matches a data field that is only a single $(dotID) +// reference with no nested keypath (dotID: [a-zA-Z0-9_]+). +var bridgeDataRootVarRegexp = regexp.MustCompile(`^\$\(\s*([a-zA-Z0-9_]+)\s*\)$`) + +func jsonParseLaxStatic(j *JSONParseTask) bool { + trimmed := strings.TrimSpace(j.Lax) + if trimmed == "" { + return false + } + b, err := strconv.ParseBool(trimmed) + return err == nil && b +} + +func jsonParseDataReferencesBridge(j *JSONParseTask, bridgeID int, bridgeDot string) bool { + dataTrim := strings.TrimSpace(j.Data) + if dataTrim == "" { + src, ok := jsonParseLowestIndexPropagatingInput(j) + return ok && src.ID() == bridgeID + } + m := bridgeDataRootVarRegexp.FindStringSubmatch(dataTrim) + return len(m) == 2 && m[1] == bridgeDot +} + +func jsonParseLowestIndexPropagatingInput(j *JSONParseTask) (Task, bool) { + var ( + found bool + minOutputIdx int32 = math.MaxInt32 + src Task + ) + for _, dep := range j.Inputs() { + if !dep.PropagateResult { + continue + } + idx := dep.InputTask.OutputIndex() + if !found || idx < minOutputIdx { + found = true + minOutputIdx = idx + src = dep.InputTask + } + } + return src, found +} + +func jsonParseStaticPathSegments(j *JSONParseTask) ([]string, bool) { + if strings.TrimSpace(j.Path) == "" { + return nil, false + } + sep := strings.TrimSpace(j.Separator) + if sep == "" { + sep = "," + } + parts := strings.Split(j.Path, sep) + if len(parts) == 0 { + return nil, false + } + return parts, true +} + +// jsonDecodeValidateRequiredPaths checks each path in the raw JSON body using +// streaming lookup (no full value unmarshal). Missing keys, JSON null, JSON numbers +// that are not valid for shopspring/decimal (including exponent/size limits), and the +// string "NaN" are treated as failure so bridge cache fallback can apply. +// +// For each present value, parseRequiredValidValue must succeed: decimals and floats +// via shopspring/decimal, integer literals (decimal or hex with optional sign) via +// math/big so huge 0x… values validate and scientific notation is not mistaken for +// hexadecimal. +func jsonDecodeValidateRequiredPaths(body []byte, paths [][]string) error { + if len(paths) == 0 { + return nil + } + for _, path := range paths { + if len(path) == 0 { + continue + } + + value, dataType, _, err := jsonparser.Get(body, path...) + if err != nil { + return errors.Wrapf(err, "required path %q", strings.Join(path, ",")) + } + + if dataType == jsonparser.Null { + return errors.Errorf("required path %q is null", strings.Join(path, ",")) + } + + if err := parseRequiredValidValue(value); err != nil { + return errors.Wrapf(err, "required path %q", strings.Join(path, ",")) + } + } + return nil +} + +// parseRequiredValidValue accepts values that can be parsed by shopspring/decimal or math/big. +func parseRequiredValidValue(value []byte) error { + strValue := string(value) + if _, err := decimal.NewFromString(strValue); err == nil { + return nil + } + if _, ok := new(big.Int).SetString(strValue, 0); ok { + return nil + } + return errors.Errorf("invalid value: %s", string(value)) +} diff --git a/core/services/pipeline/task.bridge_test.go b/core/services/pipeline/task.bridge_test.go index 9d65f1bde40..bed32e0165b 100644 --- a/core/services/pipeline/task.bridge_test.go +++ b/core/services/pipeline/task.bridge_test.go @@ -143,20 +143,20 @@ func fakePriceResponder(t *testing.T, requestData map[string]any, result decimal return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var reqBody adapterRequest payload, err := io.ReadAll(r.Body) - require.NoError(t, err) + assert.NoError(t, err) defer r.Body.Close() err = json.Unmarshal(payload, &reqBody) - require.NoError(t, err) - require.Equal(t, expectedRequest.Data, reqBody.Data) + assert.NoError(t, err) + assert.Equal(t, expectedRequest.Data, reqBody.Data) w.Header().Set("Content-Type", "application/json") - require.NoError(t, json.NewEncoder(w).Encode(response)) + assert.NoError(t, json.NewEncoder(w).Encode(response)) if inputKey != "" { m := utils.MustUnmarshalToMap(string(payload)) if expectedInput != nil { - require.Equal(t, expectedInput, m[inputKey]) + assert.Equal(t, expectedInput, m[inputKey]) } else { - require.Nil(t, m[inputKey]) + assert.Nil(t, m[inputKey]) } } }) @@ -175,28 +175,28 @@ func fakeIntermittentlyFailingPriceResponder(t *testing.T, requestData map[strin return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var reqBody adapterRequest payload, err := io.ReadAll(r.Body) - require.NoError(t, err) + assert.NoError(t, err) defer r.Body.Close() err = json.Unmarshal(payload, &reqBody) - require.NoError(t, err) - require.Equal(t, expectedRequest.Data, reqBody.Data) + assert.NoError(t, err) + assert.Equal(t, expectedRequest.Data, reqBody.Data) // require.Equal(t, float64(0), reqBody.Meta["id"]) if reqBody.Meta["shouldFail"].(bool) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadGateway) - require.NoError(t, json.NewEncoder(w).Encode(errors.New("EA failure"))) + assert.NoError(t, json.NewEncoder(w).Encode(errors.New("EA failure"))) return } w.Header().Set("Content-Type", "application/json") - require.NoError(t, json.NewEncoder(w).Encode(response)) + assert.NoError(t, json.NewEncoder(w).Encode(response)) if inputKey != "" { m := utils.MustUnmarshalToMap(string(payload)) if expectedInput != nil { - require.Equal(t, expectedInput, m[inputKey]) + assert.Equal(t, expectedInput, m[inputKey]) } else { - require.Nil(t, m[inputKey]) + assert.Nil(t, m[inputKey]) } } }) @@ -206,7 +206,7 @@ func fakeStringResponder(t *testing.T, s string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, err := w.Write([]byte(s)) - require.NoError(t, err) + assert.NoError(t, err) }) } @@ -330,6 +330,112 @@ func TestBridgeTask_HandlesIntermittentFailure(t *testing.T) { require.Equal(t, runInfo.IsRetryable, runInfo2.IsRetryable) } +func TestBridgeTask_CacheFallbackOnMissingRequiredJSONPath(t *testing.T) { + t.Parallel() + + var callCount atomic.Int32 + s1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.NoError(t, r.Body.Close()) + w.Header().Set("Content-Type", "application/json") + if callCount.Add(1) == 1 { + resp := adapterResponse{Data: dataWithResult(t, decimal.NewFromInt(42))} + assert.NoError(t, json.NewEncoder(w).Encode(resp)) + return + } + // HTTP 200 but missing data.result — should fall back to cache when required paths are set. + _, err := w.Write([]byte(`{"errorMessage":null,"error":null,"statusCode":null,"providerStatusCode":null,"data":{}}`)) + assert.NoError(t, err) + })) + defer s1.Close() + + db := pgtest.NewSqlxDB(t) + cfg := configtest.NewTestGeneralConfig(t) + feedURL, err := url.ParseRequestURI(s1.URL) + require.NoError(t, err) + orm := bridges.NewORM(db) + _, bridge := cltest.MustCreateBridge(t, db, cltest.BridgeOpts{URL: feedURL.String()}) + + task := pipeline.BridgeTask{ + BaseTask: pipeline.NewBaseTask(0, "bridge", nil, nil, 0), + Name: bridge.Name.String(), + RequestData: btcUSDPairing, + CacheTTL: "30s", + CheckRequired: "true", + } + pipeline.TestingSetBridgeRequiredJSONPaths(&task, [][]string{{"data", "result"}}) + c := clhttptest.NewTestLocalOnlyHTTPClient() + trORM := pipeline.NewORM(db, logger.TestLogger(t), cfg.JobPipeline().MaxSuccessfulRuns()) + specID, err := trORM.CreateSpec(testutils.Context(t), pipeline.Pipeline{}, *sqlutil.NewInterval(5 * time.Minute)) + require.NoError(t, err) + task.HelperSetDependencies(cfg.JobPipeline(), cfg.WebServer(), orm, specID, uuid.UUID{}, c) + + ctx := testutils.Context(t) + result, runInfo := task.Run(ctx, logger.TestLogger(t), pipeline.NewVarsFrom(nil), nil) + require.NoError(t, result.Error) + require.False(t, runInfo.IsRetryable) + + result2, runInfo2 := task.Run(ctx, logger.TestLogger(t), pipeline.NewVarsFrom(nil), nil) + require.NoError(t, result2.Error) + require.False(t, runInfo2.IsRetryable) + require.Equal(t, result.Value, result2.Value) + + var parsed struct { + Data struct { + Result decimal.Decimal `json:"result"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal([]byte(result2.Value.(string)), &parsed)) + require.Equal(t, decimal.NewFromInt(42), parsed.Data.Result) +} + +func TestBridgeTask_SkipsRequiredPathValidationWhenCheckRequiredFalse(t *testing.T) { + t.Parallel() + + var callCount atomic.Int32 + s1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.NoError(t, r.Body.Close()) + w.Header().Set("Content-Type", "application/json") + if callCount.Add(1) == 1 { + resp := adapterResponse{Data: dataWithResult(t, decimal.NewFromInt(42))} + assert.NoError(t, json.NewEncoder(w).Encode(resp)) + return + } + _, err := w.Write([]byte(`{"errorMessage":null,"error":null,"statusCode":null,"providerStatusCode":null,"data":{}}`)) + assert.NoError(t, err) + })) + defer s1.Close() + + db := pgtest.NewSqlxDB(t) + cfg := configtest.NewTestGeneralConfig(t) + feedURL, err := url.ParseRequestURI(s1.URL) + require.NoError(t, err) + orm := bridges.NewORM(db) + _, bridge := cltest.MustCreateBridge(t, db, cltest.BridgeOpts{URL: feedURL.String()}) + + task := pipeline.BridgeTask{ + BaseTask: pipeline.NewBaseTask(0, "bridge", nil, nil, 0), + Name: bridge.Name.String(), + RequestData: btcUSDPairing, + CacheTTL: "30s", + // checkRequired omitted: do not run JSON path validation or cache fallback for it. + } + pipeline.TestingSetBridgeRequiredJSONPaths(&task, [][]string{{"data", "result"}}) + c := clhttptest.NewTestLocalOnlyHTTPClient() + trORM := pipeline.NewORM(db, logger.TestLogger(t), cfg.JobPipeline().MaxSuccessfulRuns()) + specID, err := trORM.CreateSpec(testutils.Context(t), pipeline.Pipeline{}, *sqlutil.NewInterval(5 * time.Minute)) + require.NoError(t, err) + task.HelperSetDependencies(cfg.JobPipeline(), cfg.WebServer(), orm, specID, uuid.UUID{}, c) + + ctx := testutils.Context(t) + _, runInfo := task.Run(ctx, logger.TestLogger(t), pipeline.NewVarsFrom(nil), nil) + require.False(t, runInfo.IsRetryable) + + result2, runInfo2 := task.Run(ctx, logger.TestLogger(t), pipeline.NewVarsFrom(nil), nil) + require.False(t, runInfo2.IsRetryable) + require.NoError(t, result2.Error) + require.Contains(t, result2.Value.(string), `"data":{}`) +} + func TestBridgeTask_DoesNotReturnStaleResults(t *testing.T) { t.Parallel() @@ -435,17 +541,17 @@ func TestBridgeTask_AsyncJobPendingState(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var reqBody adapterRequest payload, err := io.ReadAll(r.Body) - require.NoError(t, err) + assert.NoError(t, err) defer r.Body.Close() err = json.Unmarshal(payload, &reqBody) - require.NoError(t, err) - require.Equal(t, fmt.Sprintf("%s/v2/resume/%v", cfg.WebServer().BridgeResponseURL(), id.String()), reqBody.ResponseURL) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf("%s/v2/resume/%v", cfg.WebServer().BridgeResponseURL(), id.String()), reqBody.ResponseURL) w.Header().Set("Content-Type", "application/json") // w.Header().Set("X-Chainlink-Pending", "true") response := map[string]any{"pending": true} - require.NoError(t, json.NewEncoder(w).Encode(response)) + assert.NoError(t, json.NewEncoder(w).Encode(response)) }) server := httptest.NewServer(handler) @@ -680,11 +786,11 @@ func TestBridgeTask_Meta(t *testing.T) { var req adapterRequest body, _ := io.ReadAll(r.Body) err := json.Unmarshal(body, &req) - require.NoError(t, err) - require.Equal(t, float64(10), req.Meta["latestAnswer"]) - require.Equal(t, float64(1616447984), req.Meta["updatedAt"]) + assert.NoError(t, err) + assert.InEpsilon(t, float64(10), req.Meta["latestAnswer"], 0) + assert.InDelta(t, float64(1616447984), req.Meta["updatedAt"], 0) w.Header().Set("Content-Type", "application/json") - require.NoError(t, json.NewEncoder(w).Encode(empty)) + assert.NoError(t, json.NewEncoder(w).Encode(empty)) httpCalled.Store(true) }) @@ -798,7 +904,7 @@ func TestBridgeTask_ErrorMessage(t *testing.T) { resp := &adapterResponse{} resp.SetErrorMessage("could not hit data fetcher") err := json.NewEncoder(w).Encode(resp) - require.NoError(t, err) + assert.NoError(t, err) }) server := httptest.NewServer(handler) @@ -837,7 +943,7 @@ func TestBridgeTask_OnlyErrorMessage(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadGateway) _, err := w.Write([]byte(mustReadFile(t, "../../testdata/apiresponses/coinmarketcap.error.json"))) - require.NoError(t, err) + assert.NoError(t, err) }) server := httptest.NewServer(handler) @@ -927,7 +1033,7 @@ func TestBridgeTask_Headers(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, err := w.Write([]byte(`{"fooresponse": 1}`)) - require.NoError(t, err) + assert.NoError(t, err) }) server := httptest.NewServer(handler) @@ -1039,7 +1145,7 @@ func TestBridgeTask_AdapterResponseStatusFailure(t *testing.T) { s1 := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := json.NewEncoder(w).Encode(testAdapterResponse) - require.NoError(t, err) + assert.NoError(t, err) })) defer s1.Close() @@ -1215,13 +1321,13 @@ ds [type=bridge name="adapter-error-bridge" timeout="50ms" requestData="{\"data\ bridge := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { b, herr := io.ReadAll(req.Body) - require.NoError(t, herr) - require.JSONEq(t, `{"data":{"from":"ETH","to":"USD"}}`, string(b)) + assert.NoError(t, herr) + assert.JSONEq(t, `{"data":{"from":"ETH","to":"USD"}}`, string(b)) res.WriteHeader(http.StatusInternalServerError) resp := `{"error": {"name":"AdapterLWBAError", "message": "bid ask violation detected"}}` _, herr = res.Write([]byte(resp)) - require.NoError(t, herr) + assert.NoError(t, herr) })) t.Cleanup(bridge.Close) u, _ := url.Parse(bridge.URL) diff --git a/core/services/pipeline/task.jsonparse.go b/core/services/pipeline/task.jsonparse.go index 3ba84ef047a..7382d3c6062 100644 --- a/core/services/pipeline/task.jsonparse.go +++ b/core/services/pipeline/task.jsonparse.go @@ -4,7 +4,6 @@ import ( "bytes" "context" stderrors "errors" - "math/big" "strings" "github.com/goccy/go-json" @@ -68,46 +67,9 @@ func (t *JSONParseTask) Run(_ context.Context, _ logger.Logger, vars Vars, input return Result{Error: err}, runInfo } - for _, part := range path { - switch d := decoded.(type) { - case map[string]any: - var exists bool - decoded, exists = d[part] - if !exists && bool(lax) { - decoded = nil - break - } else if !exists { - return Result{Error: errors.Wrapf(ErrKeypathNotFound, `could not resolve path ["%v"] in %s`, strings.Join(path, `","`), data)}, runInfo - } - - case []any: - bigindex, ok := big.NewInt(0).SetString(part, 10) - if !ok { - return Result{Error: errors.Wrapf(ErrKeypathNotFound, "JSONParse task error: %v is not a valid array index", part)}, runInfo - } else if !bigindex.IsInt64() { - if bool(lax) { - decoded = nil - break - } - return Result{Error: errors.Wrapf(ErrKeypathNotFound, `could not resolve path ["%v"] in %s`, strings.Join(path, `","`), data)}, runInfo - } - index := int(bigindex.Int64()) - if index < 0 { - index = len(d) + index - } - - exists := index >= 0 && index < len(d) - if !exists && bool(lax) { - decoded = nil - break - } else if !exists { - return Result{Error: errors.Wrapf(ErrKeypathNotFound, `could not resolve path ["%v"] in %s`, strings.Join(path, `","`), data)}, runInfo - } - decoded = d[index] - - default: - return Result{Error: errors.Wrapf(ErrKeypathNotFound, `could not resolve path ["%v"] in %s`, strings.Join(path, `","`), data)}, runInfo - } + decoded, err = traverseJSONPath(decoded, path, bool(lax)) + if err != nil { + return Result{Error: errors.Wrapf(err, `could not resolve path ["%v"] in %s`, strings.Join(path, `","`), data)}, runInfo } decoded, err = jsonserializable.ReinterpretJSONNumbers(decoded) diff --git a/core/web/resolver/testdata/config-empty-effective.toml b/core/web/resolver/testdata/config-empty-effective.toml index a073a30ed45..84fef8a54a1 100644 --- a/core/web/resolver/testdata/config-empty-effective.toml +++ b/core/web/resolver/testdata/config-empty-effective.toml @@ -384,6 +384,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/core/web/resolver/testdata/config-full.toml b/core/web/resolver/testdata/config-full.toml index 9fa49112c07..1d4b41e7ea1 100644 --- a/core/web/resolver/testdata/config-full.toml +++ b/core/web/resolver/testdata/config-full.toml @@ -401,6 +401,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/core/web/resolver/testdata/config-multi-chain-effective.toml b/core/web/resolver/testdata/config-multi-chain-effective.toml index afe76d5ec60..f737a33ec54 100644 --- a/core/web/resolver/testdata/config-multi-chain-effective.toml +++ b/core/web/resolver/testdata/config-multi-chain-effective.toml @@ -384,6 +384,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/core/web/testdata/body/health.html b/core/web/testdata/body/health.html index d36eae39cf5..d964b3bb929 100644 --- a/core/web/testdata/body/health.html +++ b/core/web/testdata/body/health.html @@ -93,6 +93,9 @@
JobSpawner
+
+ JobSpecReporter +
LLOTransmissionReaper
diff --git a/core/web/testdata/body/health.json b/core/web/testdata/body/health.json index df21a3bf01f..4fd58afbe16 100644 --- a/core/web/testdata/body/health.json +++ b/core/web/testdata/body/health.json @@ -171,6 +171,15 @@ "output": "" } }, + { + "type": "checks", + "id": "JobSpecReporter", + "attributes": { + "name": "JobSpecReporter", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "LLOTransmissionReaper", diff --git a/core/web/testdata/body/health.txt b/core/web/testdata/body/health.txt index b0a1b077851..ac6b5e44fb3 100644 --- a/core/web/testdata/body/health.txt +++ b/core/web/testdata/body/health.txt @@ -18,6 +18,7 @@ ok EVM.1399100.Txm.WrappedEvmEstimator ok HeadReporter ok Heartbeat ok JobSpawner +ok JobSpecReporter ok LLOTransmissionReaper ok Mailbox.Monitor ok Mercury.WSRPCPool diff --git a/deployment/go.mod b/deployment/go.mod index 26cef4bc61e..d975d0a3fab 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -36,7 +36,7 @@ require ( github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 github.com/smartcontractkit/ccip-owner-contracts v0.1.0 github.com/smartcontractkit/chain-selectors v1.0.98 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260506144252-c100eabfda74 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc @@ -430,13 +430,14 @@ require ( github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135 // indirect github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect 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/data-feeds v0.1.1-0.20260501174546-2e8846986b36 // 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-20260430172634-acccf17ece83 // indirect github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 // indirect diff --git a/deployment/go.sum b/deployment/go.sum index 6c4ae7e02d6..f8a5560d9e7 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1379,8 +1379,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.98 h1:fuI7CQ1o5cX64eO4/LvwtfhdpGFH5vnsM/bFHRwEiww= github.com/smartcontractkit/chain-selectors v1.0.98/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d h1:KMZmEzlZ+iknVFCuv+8o9iEKK52HXyeauIWbo+KhYv8= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb h1:6UjHnVanvb+6yJuefhyVlfv6YKFGMeZY5jv+a7Sexyo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb/go.mod h1:FEm5fvIQe5O8Qdx6GvQcXsk7rDFpmYdIWXea5i4tpjw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 h1:p0nFrTYrOQzDhWYm6suaM5CoWiXV5NV7llHnp6/Kn/8= @@ -1423,8 +1423,8 @@ github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-202604231355 github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c h1:0c+bCKo47vy/ItRtGa3S/vCpE5LRlgXpGnVKQX8TgjE= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c h1:1VVreRcffo3N3zF1JVtgS+YC4puuj3y0FU0Fta7L3U0= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 h1:v/rAtObo9zMpQDnGQ0seaSEqw60JUjowwcNw3DCpVY4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 h1:nXU0s4WAVU2cAR76Ke7h9z55NuEtRq1WvT4wVEs7jwk= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= @@ -1439,6 +1439,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251 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-20260505131349-78e491b80735 h1:5bxDnwI0wuPoC0H5H3H2n9CnQPb5iakR6UmAY4j8KUg= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 h1:SG+wAsNyAcA6Kk19ljuxi3HK9Ll2lpHik8OKoY4x7A0= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY= 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= diff --git a/devenv/go.mod b/devenv/go.mod index fd524205a82..aed099afcfe 100644 --- a/devenv/go.mod +++ b/devenv/go.mod @@ -27,7 +27,7 @@ require ( github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260504070828-3216bb63d886 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251211123524-f0c4fe7cfc0a github.com/smartcontractkit/chainlink-protos/job-distributor v0.12.0 - github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.17 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21 github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.14.9 github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 github.com/smartcontractkit/chainlink-testing-framework/wasp v1.51.2 diff --git a/devenv/go.sum b/devenv/go.sum index cc3f6cde67a..a3c3420edbd 100644 --- a/devenv/go.sum +++ b/devenv/go.sum @@ -1042,8 +1042,8 @@ github.com/smartcontractkit/chainlink-protos/job-distributor v0.12.0 h1:/bhoALRz github.com/smartcontractkit/chainlink-protos/job-distributor v0.12.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-testing-framework/framework v0.15.17 h1:QWXP12ewfRRVDbJcYJZBBCm5cP8Ds/KYpUc5VuAVYFk= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.17/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21 h1:lQs+r0+Jz9vFhmWPSo4bh0F1AePGIN7j++ex9cI4jA4= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.14.9 h1:MVDx/Zl7qhikAx5vQgpTiyCkXw6sXgerUAU3WyJgWVY= github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.14.9/go.mod h1:1ZKcfw6mNKvM5GNy8AjeviL0tJVZoqhLZbmskcSG68k= github.com/smartcontractkit/chainlink-testing-framework/lib/grafana v1.50.0 h1:VIxK8u0Jd0Q/VuhmsNm6Bls6Tb31H/sA3A/rbc5hnhg= diff --git a/docs/CONFIG.md b/docs/CONFIG.md index b7496871fb4..3fa35b26f09 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -2568,6 +2568,34 @@ IgnoreJoblessBridges = false # Default ``` IgnoreJoblessBridges skips bridges that have no associated jobs. +## JobSpecReporter +```toml +[JobSpecReporter] +Enabled = false # Default +PollingInterval = "1h" # Default +EnabledOCR2PluginTypes = ["median"] # Default +``` +JobSpecReporter holds settings for the Job Spec Reporter service, which periodically emits job spec telemetry. + +### Enabled +```toml +Enabled = false # Default +``` +Enabled enables the Job Spec Reporter service. + +### PollingInterval +```toml +PollingInterval = "1h" # Default +``` +PollingInterval is how often to emit a heartbeat event for each tracked job. + +### EnabledOCR2PluginTypes +```toml +EnabledOCR2PluginTypes = ["median"] # Default +``` +EnabledOCR2PluginTypes restricts OCR2 telemetry to jobs with these plugin types. +An empty list disables all OCR2 telemetry. Use ["all"] to enable all OCR2 plugin types. + ## CRE ```toml [CRE] diff --git a/go.md b/go.md index 73ec2405836..f8e3d7438b1 100644 --- a/go.md +++ b/go.md @@ -28,7 +28,7 @@ flowchart LR click ccip-owner-contracts href "https://github.com/smartcontractkit/ccip-owner-contracts" chain-selectors click chain-selectors href "https://github.com/smartcontractkit/chain-selectors" - chainlink-aptos --> chainlink-common + chainlink-aptos --> chainlink-framework/metrics click chainlink-aptos href "https://github.com/smartcontractkit/chainlink-aptos" chainlink-automation --> chainlink-common click chainlink-automation href "https://github.com/smartcontractkit/chainlink-automation" @@ -125,6 +125,8 @@ flowchart LR click chainlink-protos/chainlink-ccv/verifier href "https://github.com/smartcontractkit/chainlink-protos" chainlink-protos/cre/go --> chain-selectors click chainlink-protos/cre/go href "https://github.com/smartcontractkit/chainlink-protos" + chainlink-protos/data-feeds + click chainlink-protos/data-feeds href "https://github.com/smartcontractkit/chainlink-protos" chainlink-protos/job-distributor click chainlink-protos/job-distributor href "https://github.com/smartcontractkit/chainlink-protos" chainlink-protos/linking-service/go @@ -169,6 +171,7 @@ flowchart LR chainlink/v2 --> chainlink-data-streams chainlink/v2 --> chainlink-evm/contracts/cre/gobindings chainlink/v2 --> chainlink-feeds + chainlink/v2 --> chainlink-protos/data-feeds chainlink/v2 --> chainlink-protos/ring/go chainlink/v2 --> cre-sdk-go/capabilities/networking/http chainlink/v2 --> cre-sdk-go/capabilities/scheduler/cron @@ -251,6 +254,7 @@ flowchart LR chainlink-protos/chainlink-ccv/message-discovery chainlink-protos/chainlink-ccv/verifier chainlink-protos/cre/go + chainlink-protos/data-feeds chainlink-protos/job-distributor chainlink-protos/linking-service/go chainlink-protos/node-platform @@ -315,7 +319,7 @@ flowchart LR click ccip-owner-contracts href "https://github.com/smartcontractkit/ccip-owner-contracts" chain-selectors click chain-selectors href "https://github.com/smartcontractkit/chain-selectors" - chainlink-aptos --> chainlink-common + chainlink-aptos --> chainlink-framework/metrics click chainlink-aptos href "https://github.com/smartcontractkit/chainlink-aptos" chainlink-automation --> chainlink-common click chainlink-automation href "https://github.com/smartcontractkit/chainlink-automation" @@ -412,6 +416,8 @@ flowchart LR click chainlink-protos/chainlink-ccv/verifier href "https://github.com/smartcontractkit/chainlink-protos" chainlink-protos/cre/go --> chain-selectors click chainlink-protos/cre/go href "https://github.com/smartcontractkit/chainlink-protos" + chainlink-protos/data-feeds + click chainlink-protos/data-feeds href "https://github.com/smartcontractkit/chainlink-protos" chainlink-protos/job-distributor click chainlink-protos/job-distributor href "https://github.com/smartcontractkit/chainlink-protos" chainlink-protos/linking-service/go @@ -581,6 +587,7 @@ flowchart LR chainlink/v2 --> chainlink-data-streams chainlink/v2 --> chainlink-evm/contracts/cre/gobindings chainlink/v2 --> chainlink-feeds + chainlink/v2 --> chainlink-protos/data-feeds chainlink/v2 --> chainlink-protos/ring/go chainlink/v2 --> cre-sdk-go/capabilities/networking/http chainlink/v2 --> cre-sdk-go/capabilities/scheduler/cron @@ -709,6 +716,7 @@ flowchart LR chainlink-protos/chainlink-ccv/message-discovery chainlink-protos/chainlink-ccv/verifier chainlink-protos/cre/go + chainlink-protos/data-feeds chainlink-protos/job-distributor chainlink-protos/linking-service/go chainlink-protos/node-platform diff --git a/go.mod b/go.mod index 90ae700d30d..2dfe4c6b575 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/andybalholm/brotli v1.2.0 github.com/aptos-labs/aptos-go-sdk v1.12.1 github.com/avast/retry-go/v4 v4.7.0 + github.com/buger/jsonparser v1.1.2 github.com/buraksezer/consistent v0.10.0 github.com/cespare/xxhash/v2 v2.3.0 github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.16.2 @@ -77,7 +78,7 @@ require ( github.com/shirou/gopsutil/v3 v3.24.3 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.98 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260506144252-c100eabfda74 @@ -97,6 +98,7 @@ require ( github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735 + github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260430172634-acccf17ece83 github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 @@ -190,7 +192,6 @@ require ( github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect github.com/buger/goterm v1.0.4 // indirect - github.com/buger/jsonparser v1.1.2 // indirect github.com/bytecodealliance/wasmtime-go/v28 v28.0.0 // indirect github.com/bytedance/sonic v1.12.3 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect @@ -361,7 +362,7 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260408145530-22e2d05695cd // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect 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 diff --git a/go.sum b/go.sum index e3e4c40f715..32ecfa1c425 100644 --- a/go.sum +++ b/go.sum @@ -1228,8 +1228,8 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smartcontractkit/chain-selectors v1.0.98 h1:fuI7CQ1o5cX64eO4/LvwtfhdpGFH5vnsM/bFHRwEiww= github.com/smartcontractkit/chain-selectors v1.0.98/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d h1:KMZmEzlZ+iknVFCuv+8o9iEKK52HXyeauIWbo+KhYv8= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb h1:6UjHnVanvb+6yJuefhyVlfv6YKFGMeZY5jv+a7Sexyo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb/go.mod h1:FEm5fvIQe5O8Qdx6GvQcXsk7rDFpmYdIWXea5i4tpjw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 h1:p0nFrTYrOQzDhWYm6suaM5CoWiXV5NV7llHnp6/Kn/8= @@ -1266,8 +1266,8 @@ github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-202604231355 github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c h1:0c+bCKo47vy/ItRtGa3S/vCpE5LRlgXpGnVKQX8TgjE= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c h1:1VVreRcffo3N3zF1JVtgS+YC4puuj3y0FU0Fta7L3U0= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 h1:v/rAtObo9zMpQDnGQ0seaSEqw60JUjowwcNw3DCpVY4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 h1:nXU0s4WAVU2cAR76Ke7h9z55NuEtRq1WvT4wVEs7jwk= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= @@ -1282,6 +1282,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251 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-20260505131349-78e491b80735 h1:5bxDnwI0wuPoC0H5H3H2n9CnQPb5iakR6UmAY4j8KUg= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 h1:SG+wAsNyAcA6Kk19ljuxi3HK9Ll2lpHik8OKoY4x7A0= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY= 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-20260430172634-acccf17ece83 h1:yBSCSnJDMx7MCU0bGy6v1XK4CoP9Q7VZjjrIst4Q7rE= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 5654bf68980..9669256a2d4 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -24,7 +24,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/segmentio/ksuid v1.0.4 github.com/smartcontractkit/chain-selectors v1.0.98 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260506144252-c100eabfda74 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc @@ -412,7 +412,7 @@ require ( github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135 // indirect github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect @@ -420,6 +420,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/cre/go v0.0.0-20260505131349-78e491b80735 // indirect + github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 // 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-20260430172634-acccf17ece83 // indirect github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index eba5a3397a2..cd3583a5597 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1364,8 +1364,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.98 h1:fuI7CQ1o5cX64eO4/LvwtfhdpGFH5vnsM/bFHRwEiww= github.com/smartcontractkit/chain-selectors v1.0.98/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d h1:KMZmEzlZ+iknVFCuv+8o9iEKK52HXyeauIWbo+KhYv8= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb h1:6UjHnVanvb+6yJuefhyVlfv6YKFGMeZY5jv+a7Sexyo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb/go.mod h1:FEm5fvIQe5O8Qdx6GvQcXsk7rDFpmYdIWXea5i4tpjw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 h1:p0nFrTYrOQzDhWYm6suaM5CoWiXV5NV7llHnp6/Kn/8= @@ -1408,8 +1408,8 @@ github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-202604231355 github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c h1:0c+bCKo47vy/ItRtGa3S/vCpE5LRlgXpGnVKQX8TgjE= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c h1:1VVreRcffo3N3zF1JVtgS+YC4puuj3y0FU0Fta7L3U0= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 h1:v/rAtObo9zMpQDnGQ0seaSEqw60JUjowwcNw3DCpVY4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 h1:nXU0s4WAVU2cAR76Ke7h9z55NuEtRq1WvT4wVEs7jwk= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= @@ -1424,6 +1424,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251 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-20260505131349-78e491b80735 h1:5bxDnwI0wuPoC0H5H3H2n9CnQPb5iakR6UmAY4j8KUg= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 h1:SG+wAsNyAcA6Kk19ljuxi3HK9Ll2lpHik8OKoY4x7A0= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY= 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= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index 53d349e9864..d494d9b0106 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -15,7 +15,7 @@ require ( github.com/gagliardetto/solana-go v1.13.0 github.com/rs/zerolog v1.34.0 github.com/smartcontractkit/chain-selectors v1.0.98 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260506144252-c100eabfda74 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc @@ -489,7 +489,7 @@ require ( github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135 // indirect github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect @@ -497,6 +497,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/cre/go v0.0.0-20260505131349-78e491b80735 // indirect + github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 // 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-20260430172634-acccf17ece83 // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 1573ccf8a5d..fcb54f74401 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1632,8 +1632,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.98 h1:fuI7CQ1o5cX64eO4/LvwtfhdpGFH5vnsM/bFHRwEiww= github.com/smartcontractkit/chain-selectors v1.0.98/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d h1:KMZmEzlZ+iknVFCuv+8o9iEKK52HXyeauIWbo+KhYv8= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb h1:6UjHnVanvb+6yJuefhyVlfv6YKFGMeZY5jv+a7Sexyo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb/go.mod h1:FEm5fvIQe5O8Qdx6GvQcXsk7rDFpmYdIWXea5i4tpjw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 h1:p0nFrTYrOQzDhWYm6suaM5CoWiXV5NV7llHnp6/Kn/8= @@ -1676,8 +1676,8 @@ github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-202604231355 github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c h1:0c+bCKo47vy/ItRtGa3S/vCpE5LRlgXpGnVKQX8TgjE= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c h1:1VVreRcffo3N3zF1JVtgS+YC4puuj3y0FU0Fta7L3U0= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 h1:v/rAtObo9zMpQDnGQ0seaSEqw60JUjowwcNw3DCpVY4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 h1:nXU0s4WAVU2cAR76Ke7h9z55NuEtRq1WvT4wVEs7jwk= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= @@ -1692,6 +1692,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251 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-20260505131349-78e491b80735 h1:5bxDnwI0wuPoC0H5H3H2n9CnQPb5iakR6UmAY4j8KUg= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 h1:SG+wAsNyAcA6Kk19ljuxi3HK9Ll2lpHik8OKoY4x7A0= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY= 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= diff --git a/plugins/plugins.public.yaml b/plugins/plugins.public.yaml index 7f1efd7ab32..4fa7a98df5c 100644 --- a/plugins/plugins.public.yaml +++ b/plugins/plugins.public.yaml @@ -10,7 +10,7 @@ defaults: plugins: aptos: - moduleURI: "github.com/smartcontractkit/chainlink-aptos" - gitRef: "v0.0.0-20260506112908-f0d993b5bd6d" + gitRef: "v0.0.0-20260507123701-77fc93b573bb" installPath: "./cmd/chainlink-aptos" sui: diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index 3adbcc8ea77..240982527b3 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -31,7 +31,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/sethvargo/go-retry v0.3.0 github.com/smartcontractkit/chain-selectors v1.0.98 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 github.com/smartcontractkit/chainlink-common/keystore v1.1.0 @@ -42,7 +42,7 @@ require ( github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997 - github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.19 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21 github.com/smartcontractkit/chainlink-testing-framework/framework/components/chiprouter v1.0.3 github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.22 github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.10.0 @@ -466,13 +466,14 @@ require ( github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135 // indirect github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 // indirect github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d // indirect 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/data-feeds v0.1.1-0.20260501174546-2e8846986b36 // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260430172634-acccf17ece83 // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 // indirect github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd // indirect diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 5122bb23c50..5ae0f471bda 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1602,8 +1602,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.98 h1:fuI7CQ1o5cX64eO4/LvwtfhdpGFH5vnsM/bFHRwEiww= github.com/smartcontractkit/chain-selectors v1.0.98/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d h1:KMZmEzlZ+iknVFCuv+8o9iEKK52HXyeauIWbo+KhYv8= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb h1:6UjHnVanvb+6yJuefhyVlfv6YKFGMeZY5jv+a7Sexyo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb/go.mod h1:FEm5fvIQe5O8Qdx6GvQcXsk7rDFpmYdIWXea5i4tpjw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 h1:p0nFrTYrOQzDhWYm6suaM5CoWiXV5NV7llHnp6/Kn/8= @@ -1646,8 +1646,8 @@ github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-202604231355 github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c h1:0c+bCKo47vy/ItRtGa3S/vCpE5LRlgXpGnVKQX8TgjE= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c h1:1VVreRcffo3N3zF1JVtgS+YC4puuj3y0FU0Fta7L3U0= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 h1:v/rAtObo9zMpQDnGQ0seaSEqw60JUjowwcNw3DCpVY4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 h1:nXU0s4WAVU2cAR76Ke7h9z55NuEtRq1WvT4wVEs7jwk= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= @@ -1662,6 +1662,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251 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-20260505131349-78e491b80735 h1:5bxDnwI0wuPoC0H5H3H2n9CnQPb5iakR6UmAY4j8KUg= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 h1:SG+wAsNyAcA6Kk19ljuxi3HK9Ll2lpHik8OKoY4x7A0= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY= 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= @@ -1690,8 +1692,8 @@ github.com/smartcontractkit/chainlink-sui v0.0.0-20260429183453-39df0198aed8 h1: github.com/smartcontractkit/chainlink-sui v0.0.0-20260429183453-39df0198aed8/go.mod h1:k1HSbHyPaQWPOj6lXDIAe04EuwbC5ge1nK+cpG2E8hE= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260427132612-76b9f754a556 h1:6ocsoNPu3T0LsBiZ1tGZrjhKu8pGC1opUFz5KgHALSU= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260427132612-76b9f754a556/go.mod h1:LkUo0a46JWaCsLY4SCV5ZOESudehe2RR62C1S46iOqw= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.19 h1:inTH0/PrEaVv4iLdGsdcrP/rX7KMrq/Roosr5nIA8io= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.19/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21 h1:lQs+r0+Jz9vFhmWPSo4bh0F1AePGIN7j++ex9cI4jA4= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework/components/chiprouter v1.0.3 h1:57wap/rDhBcw6+Ld7MqwQmXt2BTyVeNGvvPFDJcO8jQ= github.com/smartcontractkit/chainlink-testing-framework/framework/components/chiprouter v1.0.3/go.mod h1:MjUJAyU+kLvLLPRPBs3X7zYadds5umZcjTLHjfNhpUc= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.22 h1:V+3clQPZ0/N8PJhJDkl00giFtsf9lv5XQJl0SCGWzcM= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index 9eb4eced4e1..12bc8e7235d 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -68,7 +68,7 @@ require ( github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997 - github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.19 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21 github.com/smartcontractkit/chainlink-testing-framework/framework/components/chiprouter v1.0.3 github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.10.0 github.com/smartcontractkit/chainlink-testing-framework/havoc v1.50.7 @@ -156,6 +156,7 @@ 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/data-feeds v0.1.1-0.20260501174546-2e8846986b36 // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260430172634-acccf17ece83 // indirect github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260506142558-6d6e28042110 // indirect github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260421131224-c46cbfe7bc6c // indirect @@ -601,7 +602,7 @@ 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/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 @@ -609,7 +610,7 @@ require ( github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135 // indirect github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 // indirect github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 8941d23fddd..6b9eabf2801 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1815,8 +1815,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.98 h1:fuI7CQ1o5cX64eO4/LvwtfhdpGFH5vnsM/bFHRwEiww= github.com/smartcontractkit/chain-selectors v1.0.98/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d h1:KMZmEzlZ+iknVFCuv+8o9iEKK52HXyeauIWbo+KhYv8= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260506112908-f0d993b5bd6d/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb h1:6UjHnVanvb+6yJuefhyVlfv6YKFGMeZY5jv+a7Sexyo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb/go.mod h1:FEm5fvIQe5O8Qdx6GvQcXsk7rDFpmYdIWXea5i4tpjw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260428205619-2db1389501a1 h1:p0nFrTYrOQzDhWYm6suaM5CoWiXV5NV7llHnp6/Kn/8= @@ -1859,8 +1859,8 @@ github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-202604231355 github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c h1:0c+bCKo47vy/ItRtGa3S/vCpE5LRlgXpGnVKQX8TgjE= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c h1:1VVreRcffo3N3zF1JVtgS+YC4puuj3y0FU0Fta7L3U0= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4 h1:v/rAtObo9zMpQDnGQ0seaSEqw60JUjowwcNw3DCpVY4= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260505202410-b350dca113b4/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 h1:nXU0s4WAVU2cAR76Ke7h9z55NuEtRq1WvT4wVEs7jwk= github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= @@ -1875,6 +1875,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251 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-20260505131349-78e491b80735 h1:5bxDnwI0wuPoC0H5H3H2n9CnQPb5iakR6UmAY4j8KUg= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260505131349-78e491b80735/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 h1:SG+wAsNyAcA6Kk19ljuxi3HK9Ll2lpHik8OKoY4x7A0= +github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY= 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= @@ -1903,8 +1905,8 @@ github.com/smartcontractkit/chainlink-sui v0.0.0-20260429183453-39df0198aed8 h1: github.com/smartcontractkit/chainlink-sui v0.0.0-20260429183453-39df0198aed8/go.mod h1:k1HSbHyPaQWPOj6lXDIAe04EuwbC5ge1nK+cpG2E8hE= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260427132612-76b9f754a556 h1:6ocsoNPu3T0LsBiZ1tGZrjhKu8pGC1opUFz5KgHALSU= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260427132612-76b9f754a556/go.mod h1:LkUo0a46JWaCsLY4SCV5ZOESudehe2RR62C1S46iOqw= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.19 h1:inTH0/PrEaVv4iLdGsdcrP/rX7KMrq/Roosr5nIA8io= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.19/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21 h1:lQs+r0+Jz9vFhmWPSo4bh0F1AePGIN7j++ex9cI4jA4= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.21/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework/components/chiprouter v1.0.3 h1:57wap/rDhBcw6+Ld7MqwQmXt2BTyVeNGvvPFDJcO8jQ= github.com/smartcontractkit/chainlink-testing-framework/framework/components/chiprouter v1.0.3/go.mod h1:MjUJAyU+kLvLLPRPBs3X7zYadds5umZcjTLHjfNhpUc= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.22 h1:V+3clQPZ0/N8PJhJDkl00giFtsf9lv5XQJl0SCGWzcM= diff --git a/testdata/scripts/config/merge_raw_configs.txtar b/testdata/scripts/config/merge_raw_configs.txtar index 375ee9b1a9c..05999eb8ff7 100644 --- a/testdata/scripts/config/merge_raw_configs.txtar +++ b/testdata/scripts/config/merge_raw_configs.txtar @@ -531,6 +531,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/testdata/scripts/health/default.txtar b/testdata/scripts/health/default.txtar index 076d109f206..a5dafbcd92b 100644 --- a/testdata/scripts/health/default.txtar +++ b/testdata/scripts/health/default.txtar @@ -37,6 +37,7 @@ ok CRE.DispatcherWrapper ok HeadReporter ok Heartbeat ok JobSpawner +ok JobSpecReporter ok LLOTransmissionReaper ok Mailbox.Monitor ok Mercury.WSRPCPool @@ -107,6 +108,15 @@ ok WorkflowStore "output": "" } }, + { + "type": "checks", + "id": "JobSpecReporter", + "attributes": { + "name": "JobSpecReporter", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "LLOTransmissionReaper", diff --git a/testdata/scripts/health/multi-chain-loopp.txtar b/testdata/scripts/health/multi-chain-loopp.txtar index e4d05c45eed..c265928b150 100644 --- a/testdata/scripts/health/multi-chain-loopp.txtar +++ b/testdata/scripts/health/multi-chain-loopp.txtar @@ -138,6 +138,7 @@ ok EVM.1.RelayerService.PluginRelayerClient.PluginEVM.Txm.WrappedEvmEstimator ok HeadReporter ok Heartbeat ok JobSpawner +ok JobSpecReporter ok LLOTransmissionReaper ok Mailbox.Monitor ok Mercury.WSRPCPool @@ -500,6 +501,15 @@ ok WorkflowStore "output": "" } }, + { + "type": "checks", + "id": "JobSpecReporter", + "attributes": { + "name": "JobSpecReporter", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "LLOTransmissionReaper", diff --git a/testdata/scripts/health/multi-chain.txtar b/testdata/scripts/health/multi-chain.txtar index f7417afe2cb..2c26bbe28b1 100644 --- a/testdata/scripts/health/multi-chain.txtar +++ b/testdata/scripts/health/multi-chain.txtar @@ -67,6 +67,7 @@ ok EVM.1.Txm.WrappedEvmEstimator ok HeadReporter ok Heartbeat ok JobSpawner +ok JobSpecReporter ok LLOTransmissionReaper ok Mailbox.Monitor ok Mercury.WSRPCPool @@ -258,6 +259,15 @@ ok WorkflowStore "output": "" } }, + { + "type": "checks", + "id": "JobSpecReporter", + "attributes": { + "name": "JobSpecReporter", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "LLOTransmissionReaper", diff --git a/testdata/scripts/node/validate/default.txtar b/testdata/scripts/node/validate/default.txtar index f3d2d8bd42d..aaf79909497 100644 --- a/testdata/scripts/node/validate/default.txtar +++ b/testdata/scripts/node/validate/default.txtar @@ -396,6 +396,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/testdata/scripts/node/validate/defaults-override.txtar b/testdata/scripts/node/validate/defaults-override.txtar index 1c1095d4f6e..819b6da4c1d 100644 --- a/testdata/scripts/node/validate/defaults-override.txtar +++ b/testdata/scripts/node/validate/defaults-override.txtar @@ -457,6 +457,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar index 4f38567ac4d..bab7d2e8676 100644 --- a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar @@ -440,6 +440,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar index 8ebd43333a2..e26feb4c031 100644 --- a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar @@ -440,6 +440,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/testdata/scripts/node/validate/disk-based-logging.txtar b/testdata/scripts/node/validate/disk-based-logging.txtar index 4f9631aff06..274bd6895ba 100644 --- a/testdata/scripts/node/validate/disk-based-logging.txtar +++ b/testdata/scripts/node/validate/disk-based-logging.txtar @@ -440,6 +440,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/testdata/scripts/node/validate/fallback-override.txtar b/testdata/scripts/node/validate/fallback-override.txtar index df43ea5f0b4..ee3727ad0f1 100644 --- a/testdata/scripts/node/validate/fallback-override.txtar +++ b/testdata/scripts/node/validate/fallback-override.txtar @@ -540,6 +540,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/testdata/scripts/node/validate/invalid-ocr-p2p.txtar b/testdata/scripts/node/validate/invalid-ocr-p2p.txtar index 714b54d7e75..c1e213fccd2 100644 --- a/testdata/scripts/node/validate/invalid-ocr-p2p.txtar +++ b/testdata/scripts/node/validate/invalid-ocr-p2p.txtar @@ -425,6 +425,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/testdata/scripts/node/validate/invalid.txtar b/testdata/scripts/node/validate/invalid.txtar index 28a2b2ff3e3..25e32cc2646 100644 --- a/testdata/scripts/node/validate/invalid.txtar +++ b/testdata/scripts/node/validate/invalid.txtar @@ -436,6 +436,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/testdata/scripts/node/validate/valid.txtar b/testdata/scripts/node/validate/valid.txtar index bf73f4924cf..2157d1ffd4e 100644 --- a/testdata/scripts/node/validate/valid.txtar +++ b/testdata/scripts/node/validate/valid.txtar @@ -437,6 +437,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876 diff --git a/testdata/scripts/node/validate/warnings.txtar b/testdata/scripts/node/validate/warnings.txtar index 973da76176b..e4cc6e32476 100644 --- a/testdata/scripts/node/validate/warnings.txtar +++ b/testdata/scripts/node/validate/warnings.txtar @@ -419,6 +419,11 @@ PollingInterval = '5m0s' IgnoreInvalidBridges = true IgnoreJoblessBridges = false +[JobSpecReporter] +Enabled = false +PollingInterval = '1h0m0s' +EnabledOCR2PluginTypes = ['median'] + [Sharding] ShardingEnabled = false ArbiterPort = 9876