diff --git a/.changeset/angry-spiders-report.md b/.changeset/angry-spiders-report.md new file mode 100644 index 00000000000..8c7e53c0123 --- /dev/null +++ b/.changeset/angry-spiders-report.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal handle new metadata fields diff --git a/.changeset/breezy-pears-cough.md b/.changeset/breezy-pears-cough.md new file mode 100644 index 00000000000..f51c952ad95 --- /dev/null +++ b/.changeset/breezy-pears-cough.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +check data size #bugfix diff --git a/.changeset/cyan-items-lie.md b/.changeset/cyan-items-lie.md new file mode 100644 index 00000000000..a15954c2951 --- /dev/null +++ b/.changeset/cyan-items-lie.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#breaking_change Remove the `xdai` `ChainType` config option. Moving forward, only `gnosis` can be used. diff --git a/.changeset/dry-doors-do.md b/.changeset/dry-doors-do.md new file mode 100644 index 00000000000..abc03c9489b --- /dev/null +++ b/.changeset/dry-doors-do.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Decouple evm config tests from core #internal diff --git a/.changeset/eleven-terms-explain.md b/.changeset/eleven-terms-explain.md new file mode 100644 index 00000000000..abcca23add3 --- /dev/null +++ b/.changeset/eleven-terms-explain.md @@ -0,0 +1,6 @@ +--- +"chainlink": patch +--- + +#bugfix +fix a funding bug in LinkAvailableBalanceMonitor diff --git a/.changeset/fast-kings-compete.md b/.changeset/fast-kings-compete.md new file mode 100644 index 00000000000..941e8a802c3 --- /dev/null +++ b/.changeset/fast-kings-compete.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal keystone: handle local target transmission logic in capability wrapper diff --git a/.changeset/few-suns-occur.md b/.changeset/few-suns-occur.md new file mode 100644 index 00000000000..a0b55e27e61 --- /dev/null +++ b/.changeset/few-suns-occur.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal remote target capability and transmission protocol diff --git a/.changeset/gentle-walls-rest.md b/.changeset/gentle-walls-rest.md new file mode 100644 index 00000000000..acdc4b484be --- /dev/null +++ b/.changeset/gentle-walls-rest.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal Pre-process contract abis in the evm chainwriter. diff --git a/.changeset/heavy-mails-rule.md b/.changeset/heavy-mails-rule.md new file mode 100644 index 00000000000..fdb6b3929b3 --- /dev/null +++ b/.changeset/heavy-mails-rule.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Add logs for when the assumptions of how the log buffer will be used are violated #internal diff --git a/.changeset/kind-cobras-hope.md b/.changeset/kind-cobras-hope.md new file mode 100644 index 00000000000..deb4f5aeea1 --- /dev/null +++ b/.changeset/kind-cobras-hope.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal fix for workflow step persistence diff --git a/.changeset/large-plants-count.md b/.changeset/large-plants-count.md new file mode 100644 index 00000000000..e300a33d3e4 --- /dev/null +++ b/.changeset/large-plants-count.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal Added a configuration option to chain writer to set the tx send strategy. diff --git a/.changeset/lazy-ravens-hide.md b/.changeset/lazy-ravens-hide.md new file mode 100644 index 00000000000..45bd44646e1 --- /dev/null +++ b/.changeset/lazy-ravens-hide.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +bump chainlink-starknet so it builds reports with median gas price #updated diff --git a/.changeset/neat-zebras-fix.md b/.changeset/neat-zebras-fix.md new file mode 100644 index 00000000000..022fd44a137 --- /dev/null +++ b/.changeset/neat-zebras-fix.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#updated Remove deprecated app.shortcut links diff --git a/.changeset/olive-knives-happen.md b/.changeset/olive-knives-happen.md new file mode 100644 index 00000000000..7f522c96ff1 --- /dev/null +++ b/.changeset/olive-knives-happen.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal Generic Plugin `onchainSigningStrategy` support diff --git a/.changeset/orange-years-divide.md b/.changeset/orange-years-divide.md new file mode 100644 index 00000000000..b9ab8acffca --- /dev/null +++ b/.changeset/orange-years-divide.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +update finality depth for chains #nops diff --git a/.changeset/real-tools-tap.md b/.changeset/real-tools-tap.md new file mode 100644 index 00000000000..37a3cf5e581 --- /dev/null +++ b/.changeset/real-tools-tap.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal added tests for Chainwriter diff --git a/.changeset/rich-melons-sin.md b/.changeset/rich-melons-sin.md new file mode 100644 index 00000000000..38fb0b0051f --- /dev/null +++ b/.changeset/rich-melons-sin.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#db_update add persistence for DON-2-DON discovery announcements diff --git a/.changeset/rude-bulldogs-draw.md b/.changeset/rude-bulldogs-draw.md new file mode 100644 index 00000000000..709c3af867d --- /dev/null +++ b/.changeset/rude-bulldogs-draw.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +Removed deprecated evm client code #internal diff --git a/.changeset/tricky-flowers-exist.md b/.changeset/tricky-flowers-exist.md new file mode 100644 index 00000000000..0b45b116f54 --- /dev/null +++ b/.changeset/tricky-flowers-exist.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#added A ChainWriter implementation in the EVM relay. diff --git a/.changeset/tricky-panthers-rush.md b/.changeset/tricky-panthers-rush.md new file mode 100644 index 00000000000..52b35eaec97 --- /dev/null +++ b/.changeset/tricky-panthers-rush.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal #bugfix Fix target wrapper init problems diff --git a/.changeset/twenty-pets-look.md b/.changeset/twenty-pets-look.md new file mode 100644 index 00000000000..e2b6eaace9d --- /dev/null +++ b/.changeset/twenty-pets-look.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#internal keystone report context diff --git a/.github/workflows/automation-nightly-tests.yml b/.github/workflows/automation-nightly-tests.yml index 5f4c6c22075..c4c6a4be063 100644 --- a/.github/workflows/automation-nightly-tests.yml +++ b/.github/workflows/automation-nightly-tests.yml @@ -96,11 +96,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-upgrade-config with: diff --git a/.github/workflows/automation-ondemand-tests.yml b/.github/workflows/automation-ondemand-tests.yml index 565db42dfc5..a42ddf81e56 100644 --- a/.github/workflows/automation-ondemand-tests.yml +++ b/.github/workflows/automation-ondemand-tests.yml @@ -226,11 +226,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML config env: SELECTED_NETWORKS: ${{ matrix.tests.network }} diff --git a/.github/workflows/ci-core.yml b/.github/workflows/ci-core.yml index be7145ab0e3..ee44c1bc46f 100644 --- a/.github/workflows/ci-core.yml +++ b/.github/workflows/ci-core.yml @@ -75,7 +75,7 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.QA_SLACK_API_KEY }} with: channel-id: "#team-core" - slack-message: "golangci-lint failed: ${{ job.html_url }}\n${{ format('https://github.com/smartcontractkit/chainlink/actions/runs/{0}', github.run_id) }}" + slack-message: "golangci-lint failed: \n${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }}" core: env: @@ -209,7 +209,7 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.QA_SLACK_API_KEY }} with: channel-id: "#topic-data-races" - slack-message: "Race tests failed: ${{ job.html_url }}\n${{ format('https://github.com/smartcontractkit/chainlink/actions/runs/{0}', github.run_id) }}" + slack-message: "Race tests failed: \n${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }}" - name: Collect Path Output id: collect-path-output env: diff --git a/.github/workflows/evm-version-compatibility-tests.yml b/.github/workflows/evm-version-compatibility-tests.yml index 8dbb1820f02..d67abb6a24c 100644 --- a/.github/workflows/evm-version-compatibility-tests.yml +++ b/.github/workflows/evm-version-compatibility-tests.yml @@ -237,11 +237,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config with: diff --git a/.github/workflows/integration-tests-publish.yml b/.github/workflows/integration-tests-publish.yml index e730a404532..f3eac97ffb5 100644 --- a/.github/workflows/integration-tests-publish.yml +++ b/.github/workflows/integration-tests-publish.yml @@ -54,7 +54,7 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.QA_SLACK_API_KEY }} with: channel-id: "#team-test-tooling-internal" - slack-message: ":x: :mild-panic-intensifies: Publish Integration Test Image failed: ${{ job.html_url }}\n${{ format('https://github.com/smartcontractkit/chainlink/actions/runs/{0}', github.run_id) }}" + slack-message: ":x: :mild-panic-intensifies: Publish Integration Test Image failed: \n${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }}" build-chainlink-image: environment: integration # Only run this build for workflow_dispatch diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8e51bf45f80..d57ac9c26e2 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -28,7 +28,7 @@ on: # Only run 1 of this workflow at a time per PR concurrency: - group: integration-tests-chainlink-${{ github.ref }}-${{ inputs.distinct_run_name }} + group: ${{ github.ref }}-${{ github.repository }}-${{ github.event_name }}--e2e-tests-${{ inputs.distinct_run_name }} cancel-in-progress: true env: @@ -330,11 +330,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config with: @@ -450,11 +445,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config with: @@ -678,11 +668,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config with: @@ -982,7 +967,7 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.QA_SLACK_API_KEY }} with: channel-id: "#team-test-tooling-internal" - slack-message: ":x: :mild-panic-intensifies: Node Migration Tests Failed: ${{ job.html_url }}\n${{ format('https://github.com/smartcontractkit/chainlink/actions/runs/{0}', github.run_id) }}" + slack-message: ":x: :mild-panic-intensifies: Node Migration Tests Failed: \n${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }}" ## Solana Section get_solana_sha: diff --git a/.github/workflows/live-testnet-tests.yml b/.github/workflows/live-testnet-tests.yml index 030e431c139..667c8a2074f 100644 --- a/.github/workflows/live-testnet-tests.yml +++ b/.github/workflows/live-testnet-tests.yml @@ -256,11 +256,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: @@ -344,11 +339,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: @@ -432,11 +422,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: @@ -520,11 +505,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: @@ -604,11 +584,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: @@ -692,11 +667,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: @@ -780,11 +750,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: @@ -868,11 +833,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: @@ -952,11 +912,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: @@ -1036,11 +991,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: @@ -1120,11 +1070,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: diff --git a/.github/workflows/live-vrf-tests.yml b/.github/workflows/live-vrf-tests.yml index f06fe8759cf..745d7db19fb 100644 --- a/.github/workflows/live-vrf-tests.yml +++ b/.github/workflows/live-vrf-tests.yml @@ -147,11 +147,6 @@ jobs: api-gateway-host: ${{ secrets.AWS_API_GW_HOST_GRAFANA }} # other inputs duplicate-authorization-header: "true" - # metrics inputs - metrics-job-name: "grafana" - gc-host: ${{ secrets.GRAFANA_INTERNAL_HOST }} - gc-basic-auth: ${{ secrets.GRAFANA_INTERNAL_URL_SHORTENER_TOKEN }} - gc-org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - name: Prepare Base64 TOML override uses: ./.github/actions/setup-create-base64-config-live-testnets with: diff --git a/common/config/chaintype.go b/common/config/chaintype.go index 1ddb3f626b5..8c89aeb4ecd 100644 --- a/common/config/chaintype.go +++ b/common/config/chaintype.go @@ -47,7 +47,7 @@ func ChainTypeFromSlug(slug string) ChainType { return ChainArbitrum case "celo": return ChainCelo - case "gnosis", "xdai": + case "gnosis": return ChainGnosis case "kroma": return ChainKroma diff --git a/contracts/.changeset/moody-days-share.md b/contracts/.changeset/moody-days-share.md new file mode 100644 index 00000000000..6c0eb4780b4 --- /dev/null +++ b/contracts/.changeset/moody-days-share.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': patch +--- + +#internal stub of keystone feed consumer contract diff --git a/contracts/.changeset/smart-trainers-begin.md b/contracts/.changeset/smart-trainers-begin.md new file mode 100644 index 00000000000..5e76abd21ef --- /dev/null +++ b/contracts/.changeset/smart-trainers-begin.md @@ -0,0 +1,5 @@ +--- +"@chainlink/contracts": patch +--- + +check data size #bugfix diff --git a/contracts/.changeset/tricky-meals-decide.md b/contracts/.changeset/tricky-meals-decide.md new file mode 100644 index 00000000000..afbb24ad9fd --- /dev/null +++ b/contracts/.changeset/tricky-meals-decide.md @@ -0,0 +1,6 @@ +--- +"@chainlink/contracts": patch +--- + +#bugfix +fix a funding bug in LinkAvailableBalanceMonitor diff --git a/contracts/scripts/native_solc_compile_all_automation b/contracts/scripts/native_solc_compile_all_automation index 5981074aa10..60b57e71cef 100755 --- a/contracts/scripts/native_solc_compile_all_automation +++ b/contracts/scripts/native_solc_compile_all_automation @@ -103,5 +103,6 @@ compileContract automation/dev/v2_3/AutomationRegistrar2_3.sol compileContract automation/dev/v2_3/AutomationRegistry2_3.sol compileContract automation/dev/v2_3/AutomationRegistryLogicA2_3.sol compileContract automation/dev/v2_3/AutomationRegistryLogicB2_3.sol +compileContract automation/dev/v2_3/AutomationRegistryLogicC2_3.sol compileContract automation/dev/v2_3/AutomationUtils2_3.sol compileContract automation/dev/interfaces/v2_3/IAutomationRegistryMaster2_3.sol diff --git a/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol b/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol index 8dbe7f54fa2..915972d7034 100644 --- a/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol +++ b/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol @@ -1611,6 +1611,43 @@ contract BillingOverrides is SetUp { } contract Transmit is SetUp { + function test_transmitRevertWithExtraBytes() external { + bytes32[3] memory exampleReportContext = [ + bytes32(0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef), + bytes32(0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890), + bytes32(0x7890abcdef1234567890abcdef1234567890abcdef1234567890abcdef123456) + ]; + + bytes memory exampleRawReport = hex"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + + bytes32[] memory exampleRs = new bytes32[](3); + exampleRs[0] = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234); + exampleRs[1] = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234); + exampleRs[2] = bytes32(0x7890789078907890789078907890789078907890789078907890789078907890); + + bytes32[] memory exampleSs = new bytes32[](3); + exampleSs[0] = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234); + exampleSs[1] = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234); + exampleSs[2] = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234); + + bytes32 exampleRawVs = bytes32(0x1234561234561234561234561234561234561234561234561234561234561234); + + bytes memory transmitData = abi.encodeWithSelector( + registry.transmit.selector, + exampleReportContext, + exampleRawReport, + exampleRs, + exampleSs, + exampleRawVs + ); + bytes memory badTransmitData = bytes.concat(transmitData, bytes1(0x00)); // add extra data + vm.startPrank(TRANSMITTERS[0]); + (bool success, bytes memory returnData) = address(registry).call(badTransmitData); // send the bogus transmit + assertFalse(success, "Call did not revert as expected"); + assertEq(returnData, abi.encodePacked(Registry.InvalidDataLength.selector)); + vm.stopPrank(); + } + function test_handlesMixedBatchOfBillingTokens() external { uint256[] memory prevUpkeepBalances = new uint256[](3); prevUpkeepBalances[0] = registry.getBalance(linkUpkeepID); diff --git a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistry2_3.sol b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistry2_3.sol index f65eaf76d9f..ba19fa2d556 100644 --- a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistry2_3.sol +++ b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistry2_3.sol @@ -90,6 +90,11 @@ contract AutomationRegistry2_3 is AutomationRegistryBase2_3, OCR2Abstract, Chain bytes32 rawVs ) external override { uint256 gasOverhead = gasleft(); + // use this msg.data length check to ensure no extra data is included in the call + // 4 is first 4 bytes of the keccak-256 hash of the function signature. ss.length == rs.length so use one of them + // 4 + (32 * 3) + (rawReport.length + 32 + 32) + (32 * rs.length + 32 + 32) + (32 * ss.length + 32 + 32) + 32 + uint256 requiredLength = 324 + rawReport.length + 64 * rs.length; + if (msg.data.length != requiredLength) revert InvalidDataLength(); HotVars memory hotVars = s_hotVars; if (hotVars.paused) revert RegistryPaused(); diff --git a/contracts/src/v0.8/automation/upkeeps/LinkAvailableBalanceMonitor.sol b/contracts/src/v0.8/automation/upkeeps/LinkAvailableBalanceMonitor.sol index 6bf74de275d..5d8a8d58c8d 100644 --- a/contracts/src/v0.8/automation/upkeeps/LinkAvailableBalanceMonitor.sol +++ b/contracts/src/v0.8/automation/upkeeps/LinkAvailableBalanceMonitor.sol @@ -32,7 +32,7 @@ interface ILinkAvailable { /// at your own risk!!! /// @dev some areas for improvement / acknowledgement of limitations: /// validate that all addresses conform to interface when adding them to the watchlist -/// this is a "trusless" upkeep, meaning it does not trust the caller of performUpkeep; +/// this is a "trustless" upkeep, meaning it does not trust the caller of performUpkeep; /// we could save a fair amount of gas and re-write this upkeep for use with Automation v2.0+, /// which has significantly different trust assumptions contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInterface, Pausable { @@ -47,7 +47,7 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter event MinWaitPeriodSet(uint256 s_minWaitPeriodSeconds, uint256 minWaitPeriodSeconds); event TopUpBlocked(address indexed topUpAddress); event TopUpFailed(address indexed recipient); - event TopUpSucceeded(address indexed topUpAddress); + event TopUpSucceeded(address indexed topUpAddress, uint256 amount); event TopUpUpdated(address indexed addr, uint256 oldTopUpAmount, uint256 newTopUpAmount); event WatchlistUpdated(); @@ -170,9 +170,9 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter /// @param targetAddress the address to be added to the watchlist /// @param dstChainSelector carries a non-zero value in case the targetAddress is an onRamp, otherwise it carries a 0 /// @dev this function has to be compatible with the event onRampSet(address, dstChainSelector) emitted by - /// the CCIP router. Important detail to know is this event is also emitted when an onRamp is decomissioned, + /// the CCIP router. Important detail to know is this event is also emitted when an onRamp is decommissioned, /// in which case it will carry the proper dstChainSelector along with the 0x0 address - function addToWatchListOrDecomission(address targetAddress, uint64 dstChainSelector) public onlyAdminOrExecutor { + function addToWatchListOrDecommission(address targetAddress, uint64 dstChainSelector) public onlyAdminOrExecutor { if (s_targets[targetAddress].isActive) revert DuplicateAddress(targetAddress); if (targetAddress == address(0) && dstChainSelector == 0) revert InvalidAddress(targetAddress); bool onRampExists = s_onRampAddresses.contains(dstChainSelector); @@ -195,7 +195,7 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter if (dstChainSelector > 0) { s_onRampAddresses.set(dstChainSelector, targetAddress); } - // else if is refundant as this is the only corner case left, maintaining it for legibility + // else if is redundant as this is the only corner case left, maintaining it for legibility } else if (targetAddress == address(0) && dstChainSelector > 0) { s_onRampAddresses.remove(dstChainSelector); } @@ -227,21 +227,21 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter uint256 minWaitPeriod = s_minWaitPeriodSeconds; address[] memory targetsToFund = new address[](maxPerform); MonitoredAddress memory contractToFund; + address targetAddress; for ( uint256 numChecked = 0; numChecked < numToCheck; (idx, numChecked) = ((idx + 1) % numTargets, numChecked + 1) ) { - address targetAddress = s_watchList.at(idx); + targetAddress = s_watchList.at(idx); contractToFund = s_targets[targetAddress]; - if ( - _needsFunding( - targetAddress, - contractToFund.lastTopUpTimestamp + minWaitPeriod, - contractToFund.minBalance, - contractToFund.isActive - ) - ) { + (bool fundingNeeded, ) = _needsFunding( + targetAddress, + contractToFund.lastTopUpTimestamp + minWaitPeriod, + contractToFund.minBalance, + contractToFund.isActive + ); + if (fundingNeeded) { targetsToFund[numFound] = targetAddress; numFound++; if (numFound == maxPerform) { @@ -266,65 +266,64 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter for (uint256 idx = 0; idx < targetAddresses.length; idx++) { address targetAddress = targetAddresses[idx]; contractToFund = s_targets[targetAddress]; - s_targets[targetAddress].lastTopUpTimestamp = uint56(block.timestamp); - if ( - localBalance >= contractToFund.topUpAmount && - _needsFunding( - targetAddress, - contractToFund.lastTopUpTimestamp + minWaitPeriod, - contractToFund.minBalance, - contractToFund.isActive - ) - ) { - bool success = i_linkToken.transfer(targetAddress, contractToFund.topUpAmount); + + (bool fundingNeeded, address target) = _needsFunding( + targetAddress, + contractToFund.lastTopUpTimestamp + minWaitPeriod, + contractToFund.minBalance, + contractToFund.isActive + ); + if (localBalance >= contractToFund.topUpAmount && fundingNeeded) { + bool success = i_linkToken.transfer(target, contractToFund.topUpAmount); if (success) { localBalance -= contractToFund.topUpAmount; - emit TopUpSucceeded(targetAddress); + s_targets[targetAddress].lastTopUpTimestamp = uint56(block.timestamp); + emit TopUpSucceeded(target, contractToFund.topUpAmount); } else { - s_targets[targetAddress].lastTopUpTimestamp = contractToFund.lastTopUpTimestamp; emit TopUpFailed(targetAddress); } } else { - s_targets[targetAddress].lastTopUpTimestamp = contractToFund.lastTopUpTimestamp; emit TopUpBlocked(targetAddress); } } } /// @notice checks the target (could be direct target or IAggregatorProxy), and determines - /// if it is elligible for funding + /// if it is eligible for funding /// @param targetAddress the target to check /// @param minBalance minimum balance required for the target /// @param minWaitPeriodPassed the minimum wait period (target lastTopUpTimestamp + minWaitPeriod) /// @return bool whether the target needs funding or not + /// @return address the address to fund. for DF, this is the aggregator address behind the proxy address. + /// for other products, it's the original target address function _needsFunding( address targetAddress, uint256 minWaitPeriodPassed, uint256 minBalance, bool contractIsActive - ) private view returns (bool) { + ) private view returns (bool, address) { // Explicitly check if the targetAddress is the zero address // or if it's not a contract. In both cases return with false, // to prevent target.linkAvailableForPayment from running, // which would revert the operation. if (targetAddress == address(0) || targetAddress.code.length == 0) { - return false; + return (false, address(0)); } ILinkAvailable target; IAggregatorProxy proxy = IAggregatorProxy(targetAddress); try proxy.aggregator() returns (address aggregatorAddress) { // proxy.aggregator() can return a 0 address if the address is not an aggregator - if (aggregatorAddress == address(0)) return false; + if (aggregatorAddress == address(0)) return (false, address(0)); target = ILinkAvailable(aggregatorAddress); } catch { target = ILinkAvailable(targetAddress); } try target.linkAvailableForPayment() returns (int256 balance) { if (balance < int256(minBalance) && minWaitPeriodPassed <= block.timestamp && contractIsActive) { - return true; + return (true, address(target)); } } catch {} - return false; + return (false, address(0)); } /// @notice Gets list of subscription ids that are underfunded and returns a keeper-compatible payload. @@ -334,9 +333,18 @@ contract LinkAvailableBalanceMonitor is AccessControl, AutomationCompatibleInter bytes calldata ) external view override whenNotPaused returns (bool upkeepNeeded, bytes memory performData) { address[] memory needsFunding = sampleUnderfundedAddresses(); - upkeepNeeded = needsFunding.length > 0; - performData = abi.encode(needsFunding); - return (upkeepNeeded, performData); + if (needsFunding.length == 0) { + return (false, ""); + } + uint96 total_batch_balance; + for (uint256 idx = 0; idx < needsFunding.length; idx++) { + address targetAddress = needsFunding[idx]; + total_batch_balance = total_batch_balance + s_targets[targetAddress].topUpAmount; + } + if (i_linkToken.balanceOf(address(this)) >= total_batch_balance) { + return (true, abi.encode(needsFunding)); + } + return (false, ""); } /// @notice Called by the keeper to send funds to underfunded addresses. diff --git a/contracts/src/v0.8/keystone/KeystoneFeedsConsumer.sol b/contracts/src/v0.8/keystone/KeystoneFeedsConsumer.sol new file mode 100644 index 00000000000..36058d2b11a --- /dev/null +++ b/contracts/src/v0.8/keystone/KeystoneFeedsConsumer.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IReceiver} from "./interfaces/IReceiver.sol"; + +contract KeystoneFeedsConsumer is IReceiver { + event MessageReceived(bytes32 indexed workflowId, address indexed workflowOwner, uint256 nReports); + event FeedReceived(bytes32 indexed feedId, uint256 price, uint64 timestamp); + + constructor() {} + + struct FeedReport { + bytes32 FeedID; + uint256 Price; + uint64 Timestamp; + } + + function onReport(bytes32 workflowId, address workflowOwner, bytes calldata rawReport) external { + // TODO: validate sender and workflowOwner + + FeedReport[] memory feeds = abi.decode(rawReport, (FeedReport[])); + for (uint32 i = 0; i < feeds.length; i++) { + emit FeedReceived(feeds[i].FeedID, feeds[i].Price, feeds[i].Timestamp); + } + + emit MessageReceived(workflowId, workflowOwner, feeds.length); + } +} diff --git a/contracts/src/v0.8/keystone/KeystoneForwarder.sol b/contracts/src/v0.8/keystone/KeystoneForwarder.sol index e8e574cc0bc..3bdfd10fe8a 100644 --- a/contracts/src/v0.8/keystone/KeystoneForwarder.sol +++ b/contracts/src/v0.8/keystone/KeystoneForwarder.sol @@ -101,7 +101,7 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac uint256 internal constant REPORT_METADATA_LENGTH = 88; uint256 internal constant SIGNATURE_LENGTH = 65; - function setConfig(uint32 donId, uint8 f, address[] calldata signers) external nonReentrant { + function setConfig(uint32 donId, uint8 f, address[] calldata signers) external onlyOwner { if (f == 0) revert FaultToleranceMustBePositive(); if (signers.length > MAX_ORACLES) revert ExcessSigners(signers.length, MAX_ORACLES); if (signers.length <= 3 * f) revert InsufficientSigners(signers.length, 3 * f + 1); @@ -130,6 +130,7 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac function report( address receiverAddress, bytes calldata rawReport, + bytes calldata reportContext, bytes[] calldata signatures ) external nonReentrant { if (rawReport.length < REPORT_METADATA_LENGTH) { @@ -149,14 +150,14 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac // validate signatures { - bytes32 hash = keccak256(rawReport); + bytes32 completeHash = keccak256(abi.encodePacked(keccak256(rawReport), reportContext)); address[MAX_ORACLES] memory signed; uint8 index; for (uint256 i; i < signatures.length; ++i) { // TODO: is libocr-style multiple bytes32 arrays more optimal, gas-wise? (bytes32 r, bytes32 s, uint8 v) = _splitSignature(signatures[i]); - address signer = ecrecover(hash, v, r, s); + address signer = ecrecover(completeHash, v + 27, r, s); // validate signer is trusted and signature is unique index = uint8(s_configs[donId]._positions[signer]); @@ -167,9 +168,8 @@ contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterfac } } - IReceiver receiver = IReceiver(receiverAddress); bool success; - try receiver.onReport(workflowId, workflowOwner, rawReport[REPORT_METADATA_LENGTH:]) { + try IReceiver(receiverAddress).onReport(workflowId, workflowOwner, rawReport[REPORT_METADATA_LENGTH:]) { success = true; } catch { // Do nothing, success is already false diff --git a/contracts/src/v0.8/keystone/test/KeystoneForwarder_ReportTest.t.sol b/contracts/src/v0.8/keystone/test/KeystoneForwarder_ReportTest.t.sol index eecaf4fb241..bb209e4cf98 100644 --- a/contracts/src/v0.8/keystone/test/KeystoneForwarder_ReportTest.t.sol +++ b/contracts/src/v0.8/keystone/test/KeystoneForwarder_ReportTest.t.sol @@ -19,6 +19,7 @@ contract KeystoneForwarder_ReportTest is BaseTest { bytes[] internal mercuryReports = new bytes[](2); bytes internal rawReports; bytes internal report; + bytes internal reportContext = new bytes(96); uint256 internal requiredSignaturesNum = F + 1; bytes[] internal signatures = new bytes[](2); @@ -34,8 +35,11 @@ contract KeystoneForwarder_ReportTest is BaseTest { report = abi.encodePacked(workflowId, DON_ID, executionId, workflowOwner, rawReports); for (uint256 i = 0; i < requiredSignaturesNum; i++) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(s_signers[i].mockPrivateKey, keccak256(report)); - signatures[i] = bytes.concat(r, s, bytes1(v)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + s_signers[i].mockPrivateKey, + keccak256(abi.encodePacked(keccak256(report), reportContext)) + ); + signatures[i] = bytes.concat(r, s, bytes1(v - 27)); } vm.startPrank(TRANSMITTER); @@ -52,14 +56,14 @@ contract KeystoneForwarder_ReportTest is BaseTest { ); vm.expectRevert(abi.encodeWithSelector(KeystoneForwarder.InvalidDonId.selector, invalidDONId)); - s_forwarder.report(address(s_receiver), reportWithInvalidDONId, signatures); + s_forwarder.report(address(s_receiver), reportWithInvalidDONId, reportContext, signatures); } function test_RevertWhen_ReportIsMalformed() public { bytes memory shortenedReport = abi.encode(bytes32(report)); vm.expectRevert(KeystoneForwarder.InvalidReport.selector); - s_forwarder.report(address(s_receiver), shortenedReport, signatures); + s_forwarder.report(address(s_receiver), shortenedReport, reportContext, signatures); } function test_RevertWhen_TooFewSignatures() public { @@ -68,7 +72,7 @@ contract KeystoneForwarder_ReportTest is BaseTest { vm.expectRevert( abi.encodeWithSelector(KeystoneForwarder.InvalidSignatureCount.selector, F + 1, fewerSignatures.length) ); - s_forwarder.report(address(s_receiver), report, fewerSignatures); + s_forwarder.report(address(s_receiver), report, reportContext, fewerSignatures); } function test_RevertWhen_TooManySignatures() public { @@ -77,40 +81,43 @@ contract KeystoneForwarder_ReportTest is BaseTest { vm.expectRevert( abi.encodeWithSelector(KeystoneForwarder.InvalidSignatureCount.selector, F + 1, moreSignatures.length) ); - s_forwarder.report(address(s_receiver), report, moreSignatures); + s_forwarder.report(address(s_receiver), report, reportContext, moreSignatures); } function test_RevertWhen_AnySignatureIsInvalid() public { signatures[1] = abi.encode(1234); // invalid signature vm.expectRevert(abi.encodeWithSelector(KeystoneForwarder.InvalidSignature.selector, signatures[1])); - s_forwarder.report(address(s_receiver), report, signatures); + s_forwarder.report(address(s_receiver), report, reportContext, signatures); } function test_RevertWhen_AnySignerIsInvalid() public { uint256 mockPK = 999; Signer memory maliciousSigner = Signer({mockPrivateKey: mockPK, signerAddress: vm.addr(mockPK)}); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(maliciousSigner.mockPrivateKey, keccak256(report)); - signatures[1] = bytes.concat(r, s, bytes1(v)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + maliciousSigner.mockPrivateKey, + keccak256(abi.encodePacked(keccak256(report), reportContext)) + ); + signatures[1] = bytes.concat(r, s, bytes1(v - 27)); vm.expectRevert(abi.encodeWithSelector(KeystoneForwarder.InvalidSigner.selector, maliciousSigner.signerAddress)); - s_forwarder.report(address(s_receiver), report, signatures); + s_forwarder.report(address(s_receiver), report, reportContext, signatures); } function test_RevertWhen_ReportHasDuplicateSignatures() public { signatures[1] = signatures[0]; // repeat a signature vm.expectRevert(abi.encodeWithSelector(KeystoneForwarder.DuplicateSigner.selector, s_signers[0].signerAddress)); - s_forwarder.report(address(s_receiver), report, signatures); + s_forwarder.report(address(s_receiver), report, reportContext, signatures); } function test_RevertWhen_ReportAlreadyProcessed() public { - s_forwarder.report(address(s_receiver), report, signatures); + s_forwarder.report(address(s_receiver), report, reportContext, signatures); bytes32 reportId = keccak256(bytes.concat(bytes20(uint160(address(s_receiver))), executionId)); vm.expectRevert(abi.encodeWithSelector(KeystoneForwarder.ReportAlreadyProcessed.selector, reportId)); - s_forwarder.report(address(s_receiver), report, signatures); + s_forwarder.report(address(s_receiver), report, reportContext, signatures); } function test_Report_SuccessfulDelivery() public { @@ -123,7 +130,7 @@ contract KeystoneForwarder_ReportTest is BaseTest { vm.expectEmit(address(s_forwarder)); emit ReportProcessed(address(s_receiver), workflowOwner, executionId, true); - s_forwarder.report(address(s_receiver), report, signatures); + s_forwarder.report(address(s_receiver), report, reportContext, signatures); // validate transmitter was recorded address transmitter = s_forwarder.getTransmitter(address(s_receiver), executionId); diff --git a/contracts/src/v0.8/keystone/test/KeystoneForwarder_SetConfigTest.t.sol b/contracts/src/v0.8/keystone/test/KeystoneForwarder_SetConfigTest.t.sol index 0211b3e4027..b6b37faff2f 100644 --- a/contracts/src/v0.8/keystone/test/KeystoneForwarder_SetConfigTest.t.sol +++ b/contracts/src/v0.8/keystone/test/KeystoneForwarder_SetConfigTest.t.sol @@ -5,6 +5,15 @@ import {BaseTest} from "./KeystoneForwarderBaseTest.t.sol"; import {KeystoneForwarder} from "../KeystoneForwarder.sol"; contract KeystoneForwarder_SetConfigTest is BaseTest { + address internal constant STRANGER = address(2); + + function test_RevertWhen_NotOwner() public { + vm.stopPrank(); // BaseTest sets ADMIN + vm.prank(STRANGER); + vm.expectRevert(); + s_forwarder.setConfig(DON_ID, F, _getSignerAddresses()); + } + function test_RevertWhen_FaultToleranceIsZero() public { vm.expectRevert(KeystoneForwarder.FaultToleranceMustBePositive.selector); s_forwarder.setConfig(DON_ID, 0, _getSignerAddresses()); diff --git a/contracts/test/v0.8/automation/CronUpkeep.test.ts b/contracts/test/v0.8/automation/CronUpkeep.test.ts index 9c0192e61e7..7b769797f12 100644 --- a/contracts/test/v0.8/automation/CronUpkeep.test.ts +++ b/contracts/test/v0.8/automation/CronUpkeep.test.ts @@ -117,7 +117,7 @@ describe('CronUpkeep', () => { it('has a limited public ABI [ @skip-coverage ]', () => { // Casting cron is necessary due to a tricky versioning mismatch issue, likely between ethers // and typechain. Remove once the version issue is resolved. - // https://app.shortcut.com/chainlinklabs/story/21905/remove-contract-cast-in-cronupkeep-test-ts + // https://smartcontract-it.atlassian.net/browse/ARCHIVE-22094 h.publicAbi(cron as unknown as Contract, [ 's_maxJobs', 'performUpkeep', diff --git a/contracts/test/v0.8/automation/LinkAvailableBalanceMonitor.test.ts b/contracts/test/v0.8/automation/LinkAvailableBalanceMonitor.test.ts index 816cd03d4d8..f63de3498b1 100644 --- a/contracts/test/v0.8/automation/LinkAvailableBalanceMonitor.test.ts +++ b/contracts/test/v0.8/automation/LinkAvailableBalanceMonitor.test.ts @@ -182,7 +182,7 @@ describe('LinkAvailableBalanceMonitor', () => { }) describe('add funds', () => { - it('Should allow anyone to add funds', async () => { + it('should allow anyone to add funds', async () => { await lt.transfer(labm.address, oneLINK) await lt.connect(stranger).transfer(labm.address, oneLINK) }) @@ -197,7 +197,7 @@ describe('LinkAvailableBalanceMonitor', () => { assert.equal(report.topUpAmount.toString(), '100') }) - it('configuresis only callable by the owner', async () => { + it('is only callable by the owner', async () => { await expect( labm.connect(stranger).setTopUpAmount(directTarget1.address, 100), ).to.be.reverted @@ -266,7 +266,7 @@ describe('LinkAvailableBalanceMonitor', () => { await tx.wait() }) - it('Should allow the owner to withdraw', async () => { + it('should allow the owner to withdraw', async () => { const beforeBalance = await lt.balanceOf(owner.address) const tx = await labm.connect(owner).withdraw(oneLINK, owner.address) await tx.wait() @@ -277,14 +277,14 @@ describe('LinkAvailableBalanceMonitor', () => { ) }) - it('Should emit an event', async () => { + it('should emit an event', async () => { const tx = await labm.connect(owner).withdraw(oneLINK, owner.address) await expect(tx) .to.emit(labm, 'FundsWithdrawn') .withArgs(oneLINK, owner.address) }) - it('Should allow the owner to withdraw to anyone', async () => { + it('should allow the owner to withdraw to anyone', async () => { const beforeBalance = await lt.balanceOf(stranger.address) const tx = await labm.connect(owner).withdraw(oneLINK, stranger.address) await tx.wait() @@ -295,21 +295,21 @@ describe('LinkAvailableBalanceMonitor', () => { ) }) - it('Should not allow strangers to withdraw', async () => { + it('should not allow strangers to withdraw', async () => { const tx = labm.connect(stranger).withdraw(oneLINK, owner.address) await expect(tx).to.be.reverted }) }) describe('pause() / unpause()', () => { - it('Should allow owner to pause / unpause', async () => { + it('should allow owner to pause / unpause', async () => { const pauseTx = await labm.connect(owner).pause() await pauseTx.wait() const unpauseTx = await labm.connect(owner).unpause() await unpauseTx.wait() }) - it('Should not allow strangers to pause / unpause', async () => { + it('should not allow strangers to pause / unpause', async () => { const pauseTxStranger = labm.connect(stranger).pause() await expect(pauseTxStranger).to.be.reverted const pauseTxOwner = await labm.connect(owner).pause() @@ -319,7 +319,7 @@ describe('LinkAvailableBalanceMonitor', () => { }) }) - describe('setWatchList() / addToWatchListOrDecomissionOrDecomission() / removeFromWatchlist() / getWatchList()', () => { + describe('setWatchList() / addToWatchListOrDecommissionOrDecommission() / removeFromWatchlist() / getWatchList()', () => { const watchAddress1 = randAddr() const watchAddress2 = randAddr() const watchAddress3 = randAddr() @@ -331,15 +331,15 @@ describe('LinkAvailableBalanceMonitor', () => { assert.deepEqual(watchList, []) }) - it('Should allow owner to adjust the watchlist', async () => { + it('should allow owner to adjust the watchlist', async () => { // add first watchlist - let tx = await labm + await labm .connect(owner) .setWatchList([watchAddress1], [oneLINK], [oneLINK], [0]) let watchList = await labm.getWatchList() assert.deepEqual(watchList[0], watchAddress1) // add more to watchlist - tx = await labm + const tx = await labm .connect(owner) .setWatchList( [watchAddress1, watchAddress2, watchAddress3], @@ -352,8 +352,8 @@ describe('LinkAvailableBalanceMonitor', () => { assert.deepEqual(watchList, [watchAddress1, watchAddress2, watchAddress3]) }) - it('Should not allow different length arrays in the watchlist', async () => { - let tx = labm + it('should not allow different length arrays in the watchlist', async () => { + const tx = labm .connect(owner) .setWatchList( [watchAddress1, watchAddress2, watchAddress1], @@ -367,8 +367,8 @@ describe('LinkAvailableBalanceMonitor', () => { ) }) - it('Should not allow duplicates in the watchlist', async () => { - let tx = labm + it('should not allow duplicates in the watchlist', async () => { + const tx = labm .connect(owner) .setWatchList( [watchAddress1, watchAddress2, watchAddress1], @@ -381,15 +381,15 @@ describe('LinkAvailableBalanceMonitor', () => { .withArgs(watchAddress1) }) - it('Should not allow strangers to set the watchlist', async () => { + it('should not allow strangers to set the watchlist', async () => { const setTxStranger = labm .connect(stranger) .setWatchList([watchAddress1], [oneLINK], [oneLINK], [0]) await expect(setTxStranger).to.be.reverted }) - it('Should revert if any of the addresses are empty', async () => { - let tx = labm + it('should revert if any of the addresses are empty', async () => { + const tx = labm .connect(owner) .setWatchList( [watchAddress1, ethers.constants.AddressZero], @@ -403,17 +403,17 @@ describe('LinkAvailableBalanceMonitor', () => { ) }) - it('Should allow owner to add multiple addresses with dstChainSelector 0 to the watchlist', async () => { + it('should allow owner to add multiple addresses with dstChainSelector 0 to the watchlist', async () => { let tx = await labm .connect(owner) - .addToWatchListOrDecomission(watchAddress1, 0) + .addToWatchListOrDecommission(watchAddress1, 0) await tx.wait let watchList = await labm.getWatchList() assert.deepEqual(watchList[0], watchAddress1) tx = await labm .connect(owner) - .addToWatchListOrDecomission(watchAddress2, 0) + .addToWatchListOrDecommission(watchAddress2, 0) await tx.wait watchList = await labm.getWatchList() assert.deepEqual(watchList[0], watchAddress1) @@ -421,7 +421,7 @@ describe('LinkAvailableBalanceMonitor', () => { tx = await labm .connect(owner) - .addToWatchListOrDecomission(watchAddress3, 0) + .addToWatchListOrDecommission(watchAddress3, 0) await tx.wait watchList = await labm.getWatchList() assert.deepEqual(watchList[0], watchAddress1) @@ -429,10 +429,10 @@ describe('LinkAvailableBalanceMonitor', () => { assert.deepEqual(watchList[2], watchAddress3) }) - it('Should allow owner to add only one address with an unique non-zero dstChainSelector 0 to the watchlist', async () => { + it('should allow owner to add only one address with an unique non-zero dstChainSelector 0 to the watchlist', async () => { let tx = await labm .connect(owner) - .addToWatchListOrDecomission(watchAddress1, 1) + .addToWatchListOrDecommission(watchAddress1, 1) await tx.wait let watchList = await labm.getWatchList() assert.deepEqual(watchList[0], watchAddress1) @@ -443,7 +443,7 @@ describe('LinkAvailableBalanceMonitor', () => { tx = await labm .connect(owner) - .addToWatchListOrDecomission(watchAddress2, 1) + .addToWatchListOrDecommission(watchAddress2, 1) await tx.wait watchList = await labm.getWatchList() assert.deepEqual(watchList[0], watchAddress2) @@ -456,7 +456,7 @@ describe('LinkAvailableBalanceMonitor', () => { tx = await labm .connect(owner) - .addToWatchListOrDecomission(watchAddress3, 1) + .addToWatchListOrDecommission(watchAddress3, 1) await tx.wait watchList = await labm.getWatchList() assert.deepEqual(watchList[0], watchAddress3) @@ -470,25 +470,25 @@ describe('LinkAvailableBalanceMonitor', () => { assert.isFalse(report.isActive) }) - it('Should not add address 0 to the watchlist', async () => { + it('should not add address 0 to the watchlist', async () => { await labm .connect(owner) - .addToWatchListOrDecomission(ethers.constants.AddressZero, 1) + .addToWatchListOrDecommission(ethers.constants.AddressZero, 1) expect(await labm.getWatchList()).to.not.contain( ethers.constants.AddressZero, ) }) - it('Should not allow stangers to add addresses to the watchlist', async () => { + it('should not allow stangers to add addresses to the watchlist', async () => { await expect( - labm.connect(stranger).addToWatchListOrDecomission(watchAddress1, 1), + labm.connect(stranger).addToWatchListOrDecommission(watchAddress1, 1), ).to.be.reverted }) - it('Should allow owner to remove addresses from the watchlist', async () => { - let tx = await labm + it('should allow owner to remove addresses from the watchlist', async () => { + const tx = await labm .connect(owner) - .addToWatchListOrDecomission(watchAddress1, 1) + .addToWatchListOrDecommission(watchAddress1, 1) await tx.wait let watchList = await labm.getWatchList() assert.deepEqual(watchList[0], watchAddress1) @@ -496,7 +496,7 @@ describe('LinkAvailableBalanceMonitor', () => { assert.isTrue(report.isActive) // remove address - tx = await labm.connect(owner).removeFromWatchList(watchAddress1) + await labm.connect(owner).removeFromWatchList(watchAddress1) // address should be false report = await labm.getAccountInfo(watchAddress1) @@ -506,13 +506,13 @@ describe('LinkAvailableBalanceMonitor', () => { assert.deepEqual(watchList, []) }) - it('Should allow only one address per dstChainSelector', async () => { + it('should allow only one address per dstChainSelector', async () => { // add address1 - await labm.connect(owner).addToWatchListOrDecomission(watchAddress1, 1) + await labm.connect(owner).addToWatchListOrDecommission(watchAddress1, 1) expect(await labm.getWatchList()).to.contain(watchAddress1) // add address2 - await labm.connect(owner).addToWatchListOrDecomission(watchAddress2, 1) + await labm.connect(owner).addToWatchListOrDecommission(watchAddress2, 1) // only address2 has to be in the watchlist const watchlist = await labm.getWatchList() @@ -520,15 +520,15 @@ describe('LinkAvailableBalanceMonitor', () => { expect(watchlist).to.contain(watchAddress2) }) - it('Should delete the onRamp address on a zero-address with same dstChainSelector', async () => { + it('should delete the onRamp address on a zero-address with same dstChainSelector', async () => { // add address1 - await labm.connect(owner).addToWatchListOrDecomission(watchAddress1, 1) + await labm.connect(owner).addToWatchListOrDecommission(watchAddress1, 1) expect(await labm.getWatchList()).to.contain(watchAddress1) // simulates an onRampSet(zeroAddress, same dstChainSelector) await labm .connect(owner) - .addToWatchListOrDecomission(ethers.constants.AddressZero, 1) + .addToWatchListOrDecommission(ethers.constants.AddressZero, 1) // address1 should be cleaned const watchlist = await labm.getWatchList() @@ -538,7 +538,7 @@ describe('LinkAvailableBalanceMonitor', () => { }) describe('checkUpkeep() / sampleUnderfundedAddresses() [ @skip-coverage ]', () => { - it('Should return list of address that are underfunded', async () => { + it('should return list of address that are underfunded', async () => { const fundTx = await lt .connect(owner) .transfer(labm.address, oneHundredLINK) @@ -563,7 +563,25 @@ describe('LinkAvailableBalanceMonitor', () => { expect(addresses).to.deep.equalInAnyOrder(watchListAddresses) }) - it('Should omit aggregators that have sufficient funding', async () => { + it('should return false because the monitor is underfunded', async () => { + // it needs 10 LINKs to fund all 5 upkeeps, but it only has 8 LINKs + const fundTx = await lt + .connect(owner) + .transfer(labm.address, fourLINK.add(fourLINK)) + await fundTx.wait() + + await labm.setWatchList( + watchListAddresses, + watchListMinBalances, + watchListTopUpAmounts, + watchListDstChainSelectors, + ) + + const [should, _] = await labm.checkUpkeep('0x') + assert.isFalse(should) + }) + + it('should omit aggregators that have sufficient funding', async () => { const fundTx = await lt.connect(owner).transfer( labm.address, oneHundredLINK, // enough for anything that needs funding @@ -607,7 +625,7 @@ describe('LinkAvailableBalanceMonitor', () => { expect(addresses).to.deep.equalInAnyOrder([]) }) - it('Should revert when paused', async () => { + it('should revert when paused', async () => { const tx = await labm.connect(owner).pause() await tx.wait() const ethCall = labm.checkUpkeep('0x') @@ -657,17 +675,17 @@ describe('LinkAvailableBalanceMonitor', () => { topUpAmount, dstChainSelectors, ) - let watchlist = await labm.getWatchList() + const watchlist = await labm.getWatchList() expect(watchlist).to.deep.equalInAnyOrder(proxyAddresses) assert.equal(watchlist.length, minBalances.length) }) - it('Should not include more than MAX_PERFORM addresses', async () => { + it('should not include more than MAX_PERFORM addresses', async () => { const addresses = await labm.sampleUnderfundedAddresses() expect(addresses.length).to.be.lessThanOrEqual(MAX_PERFORM) }) - it('Should sample from the list of addresses pseudorandomly', async () => { + it('should sample from the list of addresses pseudorandomly', async () => { const firstAddress: string[] = [] for (let idx = 0; idx < 10; idx++) { const addresses = await labm.sampleUnderfundedAddresses() @@ -686,7 +704,7 @@ describe('LinkAvailableBalanceMonitor', () => { ) }) - it('Can check MAX_CHECK upkeeps within the allotted gas limit', async () => { + it('can check MAX_CHECK upkeeps within the allotted gas limit', async () => { for (const aggregator of aggregators) { // here we make no aggregators eligible for funding, requiring the function to // traverse the whole list @@ -715,13 +733,13 @@ describe('LinkAvailableBalanceMonitor', () => { ) }) - it('Should revert when paused', async () => { + it('should revert when paused', async () => { await labm.connect(owner).pause() const performTx = labm.connect(keeperRegistry).performUpkeep(validPayload) await expect(performTx).to.be.revertedWith(PAUSED_ERR) }) - it('Should fund the appropriate addresses', async () => { + it('should fund the appropriate addresses', async () => { await aggregator1.mock.linkAvailableForPayment.returns(zeroLINK) await aggregator2.mock.linkAvailableForPayment.returns(zeroLINK) await aggregator3.mock.linkAvailableForPayment.returns(zeroLINK) @@ -731,25 +749,25 @@ describe('LinkAvailableBalanceMonitor', () => { const fundTx = await lt.connect(owner).transfer(labm.address, tenLINK) await fundTx.wait() - h.assertLinkTokenBalance(lt, aggregator1.address, zeroLINK) - h.assertLinkTokenBalance(lt, aggregator2.address, zeroLINK) - h.assertLinkTokenBalance(lt, aggregator3.address, zeroLINK) - h.assertLinkTokenBalance(lt, directTarget1.address, zeroLINK) - h.assertLinkTokenBalance(lt, directTarget2.address, zeroLINK) + await h.assertLinkTokenBalance(lt, aggregator1.address, zeroLINK) + await h.assertLinkTokenBalance(lt, aggregator2.address, zeroLINK) + await h.assertLinkTokenBalance(lt, aggregator3.address, zeroLINK) + await h.assertLinkTokenBalance(lt, directTarget1.address, zeroLINK) + await h.assertLinkTokenBalance(lt, directTarget2.address, zeroLINK) const performTx = await labm .connect(keeperRegistry) .performUpkeep(validPayload, { gasLimit: 1_500_000 }) await performTx.wait() - h.assertLinkTokenBalance(lt, aggregator1.address, twoLINK) - h.assertLinkTokenBalance(lt, aggregator2.address, twoLINK) - h.assertLinkTokenBalance(lt, aggregator3.address, twoLINK) - h.assertLinkTokenBalance(lt, directTarget1.address, twoLINK) - h.assertLinkTokenBalance(lt, directTarget2.address, twoLINK) + await h.assertLinkTokenBalance(lt, aggregator1.address, twoLINK) + await h.assertLinkTokenBalance(lt, aggregator2.address, twoLINK) + await h.assertLinkTokenBalance(lt, aggregator3.address, twoLINK) + await h.assertLinkTokenBalance(lt, directTarget1.address, twoLINK) + await h.assertLinkTokenBalance(lt, directTarget2.address, twoLINK) }) - it('Can handle MAX_PERFORM proxies within gas limit', async () => { + it('can handle MAX_PERFORM proxies within gas limit', async () => { const MAX_PERFORM = await labm.getMaxPerform() const proxyAddresses = [] const minBalances = [] @@ -777,13 +795,13 @@ describe('LinkAvailableBalanceMonitor', () => { topUpAmount, dstChainSelectors, ) - let watchlist = await labm.getWatchList() + const watchlist = await labm.getWatchList() expect(watchlist).to.deep.equalInAnyOrder(proxyAddresses) assert.equal(watchlist.length, minBalances.length) // add funds const wl = await labm.getWatchList() - let fundsNeeded = BigNumber.from(0) + const fundsNeeded = BigNumber.from(0) for (let idx = 0; idx < wl.length; idx++) { const targetInfo = await labm.getAccountInfo(wl[idx]) const targetTopUpAmount = targetInfo.topUpAmount @@ -805,13 +823,13 @@ describe('LinkAvailableBalanceMonitor', () => { }) describe('topUp()', () => { - it('Should revert topUp address(0)', async () => { + it('should revert topUp address(0)', async () => { const tx = await labm.connect(owner).topUp([ethers.constants.AddressZero]) await expect(tx).to.emit(labm, 'TopUpBlocked') }) context('when not paused', () => { - it('Should be callable by anyone', async () => { + it('should be callable by anyone', async () => { const users = [owner, keeperRegistry, stranger] for (let idx = 0; idx < users.length; idx++) { const user = users[idx] @@ -821,7 +839,7 @@ describe('LinkAvailableBalanceMonitor', () => { }) context('when paused', () => { - it('Should be callable by no one', async () => { + it('should be callable by no one', async () => { await labm.connect(owner).pause() const users = [owner, keeperRegistry, stranger] for (let idx = 0; idx < users.length; idx++) { @@ -844,7 +862,12 @@ describe('LinkAvailableBalanceMonitor', () => { ) }) - it('Should fund the appropriate addresses', async () => { + it('should fund the appropriate addresses', async () => { + const ai1 = await labm.getAccountInfo(proxy1.address) + assert.equal(0, ai1.lastTopUpTimestamp.toNumber()) + const ai4 = await labm.getAccountInfo(directTarget1.address) + assert.equal(0, ai4.lastTopUpTimestamp.toNumber()) + const tx = await labm.connect(keeperRegistry).topUp(watchListAddresses) await aggregator1.mock.linkAvailableForPayment.returns(twoLINK) @@ -855,22 +878,34 @@ describe('LinkAvailableBalanceMonitor', () => { await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(proxy1.address) + .withArgs(aggregator1.address, twoLINK) + assert.equal( + (await lt.balanceOf(aggregator1.address)).toBigInt(), + twoLINK.toBigInt(), + ) + const targetInfo1 = await labm.getAccountInfo(proxy1.address) + assert.notEqual(0, targetInfo1.lastTopUpTimestamp.toNumber()) await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(proxy2.address) + .withArgs(aggregator2.address, twoLINK) await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(proxy3.address) + .withArgs(aggregator3.address, twoLINK) await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(directTarget1.address) + .withArgs(directTarget1.address, twoLINK) + assert.equal( + (await lt.balanceOf(directTarget1.address)).toBigInt(), + twoLINK.toBigInt(), + ) + const targetInfo4 = await labm.getAccountInfo(directTarget1.address) + assert.notEqual(0, targetInfo4.lastTopUpTimestamp.toNumber()) await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(directTarget2.address) + .withArgs(directTarget2.address, twoLINK) }) - it('Should only fund the addresses provided', async () => { + it('should only fund the addresses provided', async () => { await labm .connect(keeperRegistry) .topUp([proxy1.address, directTarget1.address]) @@ -882,7 +917,7 @@ describe('LinkAvailableBalanceMonitor', () => { await directTarget2.mock.linkAvailableForPayment.returns(zeroLINK) }) - it('Should skip un-approved addresses', async () => { + it('should skip un-approved addresses', async () => { await labm .connect(owner) .setWatchList( @@ -901,26 +936,31 @@ describe('LinkAvailableBalanceMonitor', () => { directTarget2.address, ]) - h.assertLinkTokenBalance(lt, aggregator1.address, twoLINK) - h.assertLinkTokenBalance(lt, aggregator2.address, zeroLINK) - h.assertLinkTokenBalance(lt, aggregator3.address, zeroLINK) - h.assertLinkTokenBalance(lt, directTarget1.address, twoLINK) - h.assertLinkTokenBalance(lt, directTarget2.address, zeroLINK) + await h.assertLinkTokenBalance(lt, aggregator1.address, oneLINK) + await h.assertLinkTokenBalance(lt, aggregator2.address, zeroLINK) + await h.assertLinkTokenBalance(lt, aggregator3.address, zeroLINK) + await h.assertLinkTokenBalance(lt, directTarget1.address, oneLINK) + await h.assertLinkTokenBalance(lt, directTarget2.address, zeroLINK) await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(proxy1.address) + .withArgs(aggregator1.address, oneLINK) + const targetInfo1 = await labm.getAccountInfo(proxy1.address) + assert.notEqual(0, targetInfo1.lastTopUpTimestamp.toNumber()) + await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(directTarget1.address) + .withArgs(directTarget1.address, oneLINK) await expect(tx).to.emit(labm, 'TopUpBlocked').withArgs(proxy2.address) await expect(tx).to.emit(labm, 'TopUpBlocked').withArgs(proxy3.address) await expect(tx) .to.emit(labm, 'TopUpBlocked') .withArgs(directTarget2.address) + const targetInfo5 = await labm.getAccountInfo(directTarget2.address) + assert.equal(0, targetInfo5.lastTopUpTimestamp.toNumber()) }) - it('Should skip an address if the proxy is invalid and it is not a direct target', async () => { + it('should skip an address if the proxy is invalid and it is not a direct target', async () => { await labm .connect(owner) .setWatchList( @@ -934,11 +974,11 @@ describe('LinkAvailableBalanceMonitor', () => { .topUp([proxy1.address, proxy4.address]) await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(proxy1.address) + .withArgs(aggregator1.address, oneLINK) await expect(tx).to.emit(labm, 'TopUpBlocked').withArgs(proxy4.address) }) - it('Should skip an address if the aggregator is invalid', async () => { + it('should skip an address if the aggregator is invalid', async () => { await proxy4.mock.aggregator.returns(aggregator4.address) await labm .connect(owner) @@ -953,11 +993,11 @@ describe('LinkAvailableBalanceMonitor', () => { .topUp([proxy1.address, proxy4.address]) await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(proxy1.address) + .withArgs(aggregator1.address, oneLINK) await expect(tx).to.emit(labm, 'TopUpBlocked').withArgs(proxy4.address) }) - it('Should skip an address if the aggregator has sufficient funding', async () => { + it('should skip an address if the aggregator has sufficient funding', async () => { await proxy4.mock.aggregator.returns(aggregator4.address) await aggregator4.mock.linkAvailableForPayment.returns(tenLINK) await labm @@ -973,11 +1013,11 @@ describe('LinkAvailableBalanceMonitor', () => { .topUp([proxy1.address, proxy4.address]) await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(proxy1.address) + .withArgs(aggregator1.address, oneLINK) await expect(tx).to.emit(labm, 'TopUpBlocked').withArgs(proxy4.address) }) - it('Should skip an address if the direct target has sufficient funding', async () => { + it('should skip an address if the direct target has sufficient funding', async () => { await directTarget1.mock.linkAvailableForPayment.returns(tenLINK) await labm .connect(owner) @@ -992,7 +1032,11 @@ describe('LinkAvailableBalanceMonitor', () => { .topUp([proxy1.address, directTarget1.address]) await expect(tx) .to.emit(labm, 'TopUpSucceeded') - .withArgs(proxy1.address) + .withArgs(aggregator1.address, oneLINK) + assert.equal( + (await lt.balanceOf(aggregator1.address)).toBigInt(), + oneLINK.toBigInt(), + ) await expect(tx) .to.emit(labm, 'TopUpBlocked') .withArgs(directTarget1.address) @@ -1000,7 +1044,7 @@ describe('LinkAvailableBalanceMonitor', () => { }) context('when partially funded', () => { - it('Should fund as many addresses as possible T', async () => { + it('should fund as many addresses as possible', async () => { await lt.connect(owner).transfer( labm.address, fourLINK, // only enough LINK to fund 2 addresses @@ -1012,14 +1056,21 @@ describe('LinkAvailableBalanceMonitor', () => { await directTarget1.mock.linkAvailableForPayment.returns(zeroLINK) await directTarget2.mock.linkAvailableForPayment.returns(zeroLINK) - h.assertLinkTokenBalance(lt, aggregator1.address, twoLINK) - h.assertLinkTokenBalance(lt, aggregator2.address, twoLINK) - h.assertLinkTokenBalance(lt, aggregator3.address, zeroLINK) - h.assertLinkTokenBalance(lt, directTarget1.address, zeroLINK) - h.assertLinkTokenBalance(lt, directTarget2.address, zeroLINK) - const tx = await labm.connect(keeperRegistry).topUp(watchListAddresses) - await expect(tx).to.emit(labm, 'TopUpSucceeded') + await expect(tx) + .to.emit(labm, 'TopUpSucceeded') + .withArgs(aggregator3.address, twoLINK) + await expect(tx) + .to.emit(labm, 'TopUpSucceeded') + .withArgs(directTarget1.address, twoLINK) + assert.equal( + (await lt.balanceOf(aggregator3.address)).toBigInt(), + twoLINK.toBigInt(), + ) + assert.equal( + (await lt.balanceOf(directTarget1.address)).toBigInt(), + twoLINK.toBigInt(), + ) }) }) }) diff --git a/core/capabilities/registry.go b/core/capabilities/registry.go index 3c7bdf2c971..042e5dfca90 100644 --- a/core/capabilities/registry.go +++ b/core/capabilities/registry.go @@ -12,9 +12,9 @@ import ( // Registry is a struct for the registry of capabilities. // Registry is safe for concurrent use. type Registry struct { + lggr logger.Logger m map[string]capabilities.BaseCapability mu sync.RWMutex - lggr logger.Logger } // Get gets a capability from the registry. diff --git a/core/capabilities/registry_test.go b/core/capabilities/registry_test.go index ccff0a5360e..3bed31a957a 100644 --- a/core/capabilities/registry_test.go +++ b/core/capabilities/registry_test.go @@ -42,7 +42,6 @@ func TestRegistry(t *testing.T) { capabilities.CapabilityTypeAction, "capability-1-description", "v1.0.0", - nil, ) require.NoError(t, err) @@ -71,7 +70,6 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { capabilities.CapabilityTypeAction, "capability-1-description", "v1.0.0", - nil, ) require.NoError(t, err) @@ -84,7 +82,6 @@ func TestRegistry_NoDuplicateIDs(t *testing.T) { capabilities.CapabilityTypeConsensus, "capability-2-description", "v1.0.0", - nil, ) require.NoError(t, err) c2 := &mockCapability{CapabilityInfo: ci} @@ -109,7 +106,6 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { capabilities.CapabilityTypeAction, "capability-1-description", "v1.0.0", - nil, ) require.NoError(t, err) @@ -130,7 +126,6 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { capabilities.CapabilityTypeTarget, "capability-1-description", "v1.0.0", - nil, ) require.NoError(t, err) @@ -164,7 +159,6 @@ func TestRegistry_ChecksExecutionAPIByType(t *testing.T) { capabilities.CapabilityTypeConsensus, "capability-1-description", "v1.0.0", - nil, ) require.NoError(t, err) diff --git a/core/capabilities/remote/target.go b/core/capabilities/remote/target.go deleted file mode 100644 index 655f4f84abb..00000000000 --- a/core/capabilities/remote/target.go +++ /dev/null @@ -1,87 +0,0 @@ -package remote - -import ( - "context" - "errors" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" - "github.com/smartcontractkit/chainlink/v2/core/logger" -) - -// remoteTargetCaller/Receiver are shims translating between capability API calls and network messages -type remoteTargetCaller struct { - capInfo commoncap.CapabilityInfo - donInfo *capabilities.DON - dispatcher types.Dispatcher - lggr logger.Logger -} - -var _ commoncap.TargetCapability = &remoteTargetCaller{} -var _ types.Receiver = &remoteTargetCaller{} - -type remoteTargetReceiver struct { - capInfo commoncap.CapabilityInfo - donInfo *capabilities.DON - dispatcher types.Dispatcher - lggr logger.Logger -} - -var _ types.Receiver = &remoteTargetReceiver{} - -func NewRemoteTargetCaller(capInfo commoncap.CapabilityInfo, donInfo *capabilities.DON, dispatcher types.Dispatcher, lggr logger.Logger) *remoteTargetCaller { - return &remoteTargetCaller{ - capInfo: capInfo, - donInfo: donInfo, - dispatcher: dispatcher, - lggr: lggr, - } -} - -func (c *remoteTargetCaller) Info(ctx context.Context) (commoncap.CapabilityInfo, error) { - return c.capInfo, nil -} - -func (c *remoteTargetCaller) RegisterToWorkflow(ctx context.Context, request commoncap.RegisterToWorkflowRequest) error { - return errors.New("not implemented") -} - -func (c *remoteTargetCaller) UnregisterFromWorkflow(ctx context.Context, request commoncap.UnregisterFromWorkflowRequest) error { - return errors.New("not implemented") -} - -func (c *remoteTargetCaller) Execute(ctx context.Context, request commoncap.CapabilityRequest) (<-chan commoncap.CapabilityResponse, error) { - c.lggr.Debugw("not implemented - executing fake remote target capability", "capabilityId", c.capInfo.ID, "nMembers", len(c.donInfo.Members)) - for _, peerID := range c.donInfo.Members { - m := &types.MessageBody{ - CapabilityId: c.capInfo.ID, - CapabilityDonId: c.donInfo.ID, - Payload: []byte{0x01, 0x02, 0x03}, - } - err := c.dispatcher.Send(peerID, m) - if err != nil { - return nil, err - } - } - - // TODO: return a channel that will be closed when all responses are received - return nil, nil -} - -func (c *remoteTargetCaller) Receive(msg *types.MessageBody) { - c.lggr.Debugw("not implemented - received message", "capabilityId", c.capInfo.ID, "payload", msg.Payload) -} - -func NewRemoteTargetReceiver(capInfo commoncap.CapabilityInfo, donInfo *capabilities.DON, dispatcher types.Dispatcher, lggr logger.Logger) *remoteTargetReceiver { - return &remoteTargetReceiver{ - capInfo: capInfo, - donInfo: donInfo, - dispatcher: dispatcher, - lggr: lggr, - } -} - -func (c *remoteTargetReceiver) Receive(msg *types.MessageBody) { - c.lggr.Debugw("not implemented - received message", "capabilityId", c.capInfo.ID, "payload", msg.Payload) -} diff --git a/core/capabilities/remote/target/client.go b/core/capabilities/remote/target/client.go new file mode 100644 index 00000000000..ceab11dfcb0 --- /dev/null +++ b/core/capabilities/remote/target/client.go @@ -0,0 +1,142 @@ +package target + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target/request" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +// client is a shim for remote target capabilities. +// It translates between capability API calls and network messages. +// Its responsibilities are: +// 1. Transmit capability requests to remote nodes according to a transmission schedule +// 2. Aggregate responses from remote nodes and return the aggregated response +// +// client communicates with corresponding server on remote nodes. +type client struct { + lggr logger.Logger + remoteCapabilityInfo commoncap.CapabilityInfo + localDONInfo capabilities.DON + dispatcher types.Dispatcher + requestTimeout time.Duration + + messageIDToCallerRequest map[string]*request.ClientRequest + mutex sync.Mutex +} + +var _ commoncap.TargetCapability = &client{} +var _ types.Receiver = &client{} + +func NewClient(ctx context.Context, lggr logger.Logger, remoteCapabilityInfo commoncap.CapabilityInfo, localDonInfo capabilities.DON, dispatcher types.Dispatcher, + requestTimeout time.Duration) *client { + c := &client{ + lggr: lggr, + remoteCapabilityInfo: remoteCapabilityInfo, + localDONInfo: localDonInfo, + dispatcher: dispatcher, + requestTimeout: requestTimeout, + messageIDToCallerRequest: make(map[string]*request.ClientRequest), + } + + go func() { + ticker := time.NewTicker(requestTimeout) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + c.expireRequests() + } + } + }() + + return c +} + +func (c *client) expireRequests() { + c.mutex.Lock() + defer c.mutex.Unlock() + + for messageID, req := range c.messageIDToCallerRequest { + if req.Expired() { + req.Cancel(errors.New("request expired")) + delete(c.messageIDToCallerRequest, messageID) + } + } +} + +func (c *client) Info(ctx context.Context) (commoncap.CapabilityInfo, error) { + return c.remoteCapabilityInfo, nil +} + +func (c *client) RegisterToWorkflow(ctx context.Context, request commoncap.RegisterToWorkflowRequest) error { + // do nothing + return nil +} + +func (c *client) UnregisterFromWorkflow(ctx context.Context, request commoncap.UnregisterFromWorkflowRequest) error { + // do nothing + return nil +} + +func (c *client) Execute(ctx context.Context, capReq commoncap.CapabilityRequest) (<-chan commoncap.CapabilityResponse, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + messageID, err := GetMessageIDForRequest(capReq) + if err != nil { + return nil, fmt.Errorf("failed to get message ID for request: %w", err) + } + + if _, ok := c.messageIDToCallerRequest[messageID]; ok { + return nil, fmt.Errorf("request for message ID %s already exists", messageID) + } + + req, err := request.NewClientRequest(ctx, c.lggr, capReq, messageID, c.remoteCapabilityInfo, c.localDONInfo, c.dispatcher, + c.requestTimeout) + if err != nil { + return nil, fmt.Errorf("failed to create client request: %w", err) + } + + c.messageIDToCallerRequest[messageID] = req + + return req.ResponseChan(), nil +} + +func (c *client) Receive(msg *types.MessageBody) { + c.mutex.Lock() + defer c.mutex.Unlock() + // TODO should the dispatcher be passing in a context? + ctx := context.Background() + + messageID := GetMessageID(msg) + + req := c.messageIDToCallerRequest[messageID] + if req == nil { + c.lggr.Warnw("received response for unknown message ID ", "messageID", messageID) + return + } + + go func() { + if err := req.OnMessage(ctx, msg); err != nil { + c.lggr.Errorw("failed to add response to request", "messageID", messageID, "err", err) + } + }() +} + +func GetMessageIDForRequest(req commoncap.CapabilityRequest) (string, error) { + if req.Metadata.WorkflowID == "" || req.Metadata.WorkflowExecutionID == "" { + return "", errors.New("workflow ID and workflow execution ID must be set in request metadata") + } + + return req.Metadata.WorkflowID + req.Metadata.WorkflowExecutionID, nil +} diff --git a/core/capabilities/remote/target/client_test.go b/core/capabilities/remote/target/client_test.go new file mode 100644 index 00000000000..5bfbb0c7a0c --- /dev/null +++ b/core/capabilities/remote/target/client_test.go @@ -0,0 +1,312 @@ +package target_test + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target" + remotetypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/transmission" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" +) + +func Test_Client_DonTopologies(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_OneAtATime, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + responseTest := func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error) { + require.NoError(t, responseError) + response := <-responseCh + responseValue, err := response.Value.Unwrap() + require.NoError(t, err) + assert.Equal(t, "aValue1", responseValue.(string)) + } + + capability := &TestCapability{} + + responseTimeOut := 10 * time.Minute + + testClient(ctx, t, 1, responseTimeOut, 1, 0, + capability, transmissionSchedule, responseTest) + + testClient(ctx, t, 10, responseTimeOut, 1, 0, + capability, transmissionSchedule, responseTest) + + testClient(ctx, t, 1, responseTimeOut, 10, 3, + capability, transmissionSchedule, responseTest) + + testClient(ctx, t, 10, responseTimeOut, 10, 3, + capability, transmissionSchedule, responseTest) + + testClient(ctx, t, 10, responseTimeOut, 10, 9, + capability, transmissionSchedule, responseTest) +} + +func Test_Client_TransmissionSchedules(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + responseTest := func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error) { + require.NoError(t, responseError) + response := <-responseCh + responseValue, err := response.Value.Unwrap() + require.NoError(t, err) + assert.Equal(t, "aValue1", responseValue.(string)) + } + + capability := &TestCapability{} + + responseTimeOut := 10 * time.Minute + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_OneAtATime, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + testClient(ctx, t, 1, responseTimeOut, 1, 0, + capability, transmissionSchedule, responseTest) + testClient(ctx, t, 10, responseTimeOut, 10, 3, + capability, transmissionSchedule, responseTest) + + transmissionSchedule, err = values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + testClient(ctx, t, 1, responseTimeOut, 1, 0, + capability, transmissionSchedule, responseTest) + testClient(ctx, t, 10, responseTimeOut, 10, 3, + capability, transmissionSchedule, responseTest) +} + +func Test_Client_TimesOutIfInsufficientCapabilityPeerResponses(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + responseTest := func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error) { + require.NoError(t, responseError) + response := <-responseCh + assert.NotNil(t, response.Err) + } + + capability := &TestCapability{} + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + // number of capability peers is less than F + 1 + + testClient(ctx, t, 10, 1*time.Second, 10, 11, + capability, transmissionSchedule, responseTest) +} + +func testClient(ctx context.Context, t *testing.T, numWorkflowPeers int, workflowNodeResponseTimeout time.Duration, + numCapabilityPeers int, capabilityDonF uint8, underlying commoncap.TargetCapability, transmissionSchedule *values.Map, + responseTest func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error)) { + lggr := logger.TestLogger(t) + + capabilityPeers := make([]p2ptypes.PeerID, numCapabilityPeers) + for i := 0; i < numCapabilityPeers; i++ { + capabilityPeers[i] = NewP2PPeerID(t) + } + + capDonInfo := commoncap.DON{ + ID: "capability-don", + Members: capabilityPeers, + F: capabilityDonF, + } + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Remote Target", + Version: "0.0.1", + DON: &capDonInfo, + } + + workflowPeers := make([]p2ptypes.PeerID, numWorkflowPeers) + for i := 0; i < numWorkflowPeers; i++ { + workflowPeers[i] = NewP2PPeerID(t) + } + + workflowDonInfo := commoncap.DON{ + Members: workflowPeers, + ID: "workflow-don", + } + + broker := newTestMessageBroker() + + receivers := make([]remotetypes.Receiver, numCapabilityPeers) + for i := 0; i < numCapabilityPeers; i++ { + capabilityDispatcher := broker.NewDispatcherForNode(capabilityPeers[i]) + receiver := newTestServer(capabilityPeers[i], capabilityDispatcher, workflowDonInfo, underlying) + broker.RegisterReceiverNode(capabilityPeers[i], receiver) + receivers[i] = receiver + } + + callers := make([]commoncap.TargetCapability, numWorkflowPeers) + for i := 0; i < numWorkflowPeers; i++ { + workflowPeerDispatcher := broker.NewDispatcherForNode(workflowPeers[i]) + caller := target.NewClient(ctx, lggr, capInfo, workflowDonInfo, workflowPeerDispatcher, workflowNodeResponseTimeout) + broker.RegisterReceiverNode(workflowPeers[i], caller) + callers[i] = caller + } + + executeInputs, err := values.NewMap( + map[string]any{ + "executeValue1": "aValue1", + }, + ) + + require.NoError(t, err) + + wg := &sync.WaitGroup{} + wg.Add(len(callers)) + + // Fire off all the requests + for _, caller := range callers { + go func(caller commoncap.TargetCapability) { + responseCh, err := caller.Execute(ctx, + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: "workflowID", + WorkflowExecutionID: "workflowExecutionID", + }, + Config: transmissionSchedule, + Inputs: executeInputs, + }) + + responseTest(t, responseCh, err) + wg.Done() + }(caller) + } + + wg.Wait() +} + +// Simple client that only responds once it has received a message from each workflow peer +type clientTestServer struct { + peerID p2ptypes.PeerID + dispatcher remotetypes.Dispatcher + workflowDonInfo commoncap.DON + messageIDToSenders map[string]map[p2ptypes.PeerID]bool + + targetCapability commoncap.TargetCapability + + mux sync.Mutex +} + +func newTestServer(peerID p2ptypes.PeerID, dispatcher remotetypes.Dispatcher, workflowDonInfo commoncap.DON, + targetCapability commoncap.TargetCapability) *clientTestServer { + return &clientTestServer{ + dispatcher: dispatcher, + workflowDonInfo: workflowDonInfo, + peerID: peerID, + messageIDToSenders: make(map[string]map[p2ptypes.PeerID]bool), + targetCapability: targetCapability, + } +} + +func (t *clientTestServer) Receive(msg *remotetypes.MessageBody) { + t.mux.Lock() + defer t.mux.Unlock() + + sender := toPeerID(msg.Sender) + messageID := target.GetMessageID(msg) + + if t.messageIDToSenders[messageID] == nil { + t.messageIDToSenders[messageID] = make(map[p2ptypes.PeerID]bool) + } + + sendersOfMessageID := t.messageIDToSenders[messageID] + if sendersOfMessageID[sender] { + panic("received duplicate message") + } + + sendersOfMessageID[sender] = true + + if len(t.messageIDToSenders[messageID]) == len(t.workflowDonInfo.Members) { + capabilityRequest, err := pb.UnmarshalCapabilityRequest(msg.Payload) + if err != nil { + panic(err) + } + + respCh, responseErr := t.targetCapability.Execute(context.Background(), capabilityRequest) + resp := <-respCh + + for receiver := range t.messageIDToSenders[messageID] { + var responseMsg = &remotetypes.MessageBody{ + CapabilityId: "cap_id", + CapabilityDonId: "capability-don", + CallerDonId: t.workflowDonInfo.ID, + Method: remotetypes.MethodExecute, + MessageId: []byte(messageID), + Sender: t.peerID[:], + Receiver: receiver[:], + } + + if responseErr != nil { + responseMsg.Error = remotetypes.Error_INTERNAL_ERROR + } else { + payload, marshalErr := pb.MarshalCapabilityResponse(resp) + if marshalErr != nil { + panic(marshalErr) + } + responseMsg.Payload = payload + } + + err = t.dispatcher.Send(receiver, responseMsg) + if err != nil { + panic(err) + } + } + } +} + +type TestDispatcher struct { + sentMessagesCh chan *remotetypes.MessageBody + receiver remotetypes.Receiver +} + +func NewTestDispatcher() *TestDispatcher { + return &TestDispatcher{ + sentMessagesCh: make(chan *remotetypes.MessageBody, 1), + } +} + +func (t *TestDispatcher) SendToReceiver(msgBody *remotetypes.MessageBody) { + t.receiver.Receive(msgBody) +} + +func (t *TestDispatcher) SetReceiver(capabilityId string, donId string, receiver remotetypes.Receiver) error { + t.receiver = receiver + return nil +} + +func (t *TestDispatcher) RemoveReceiver(capabilityId string, donId string) {} + +func (t *TestDispatcher) Send(peerID p2ptypes.PeerID, msgBody *remotetypes.MessageBody) error { + t.sentMessagesCh <- msgBody + return nil +} diff --git a/core/capabilities/remote/target/endtoend_test.go b/core/capabilities/remote/target/endtoend_test.go new file mode 100644 index 00000000000..998c9532871 --- /dev/null +++ b/core/capabilities/remote/target/endtoend_test.go @@ -0,0 +1,404 @@ +package target_test + +import ( + "context" + "crypto/rand" + "errors" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/mr-tron/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target" + remotetypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/transmission" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" +) + +func Test_RemoteTargetCapability_InsufficientCapabilityResponses(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + responseTest := func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error) { + require.NoError(t, responseError) + response := <-responseCh + assert.NotNil(t, response.Err) + } + + capability := &TestCapability{} + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + testRemoteTarget(ctx, t, capability, 10, 9, 10*time.Millisecond, 10, 10, 10*time.Minute, transmissionSchedule, responseTest) +} + +func Test_RemoteTargetCapability_InsufficientWorkflowRequests(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + responseTest := func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error) { + require.NoError(t, responseError) + response := <-responseCh + assert.NotNil(t, response.Err) + } + + timeOut := 10 * time.Minute + + capability := &TestCapability{} + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + testRemoteTarget(ctx, t, capability, 10, 10, 10*time.Millisecond, 10, 9, timeOut, transmissionSchedule, responseTest) +} + +func Test_RemoteTargetCapability_TransmissionSchedules(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + responseTest := func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error) { + require.NoError(t, responseError) + response := <-responseCh + responseValue, err := response.Value.Unwrap() + require.NoError(t, err) + assert.Equal(t, "aValue1", responseValue.(string)) + } + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_OneAtATime, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + timeOut := 10 * time.Minute + + capability := &TestCapability{} + + testRemoteTarget(ctx, t, capability, 10, 9, timeOut, 10, 9, timeOut, transmissionSchedule, responseTest) + + transmissionSchedule, err = values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + testRemoteTarget(ctx, t, capability, 10, 9, timeOut, 10, 9, timeOut, transmissionSchedule, responseTest) +} + +func Test_RemoteTargetCapability_DonTopologies(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + responseTest := func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error) { + require.NoError(t, responseError) + response := <-responseCh + responseValue, err := response.Value.Unwrap() + require.NoError(t, err) + assert.Equal(t, "aValue1", responseValue.(string)) + } + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_OneAtATime, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + timeOut := 10 * time.Minute + + capability := &TestCapability{} + + // Test scenarios where the number of submissions is greater than or equal to F + 1 + testRemoteTarget(ctx, t, capability, 1, 0, timeOut, 1, 0, timeOut, transmissionSchedule, responseTest) + testRemoteTarget(ctx, t, capability, 4, 3, timeOut, 1, 0, timeOut, transmissionSchedule, responseTest) + testRemoteTarget(ctx, t, capability, 10, 3, timeOut, 1, 0, timeOut, transmissionSchedule, responseTest) + + testRemoteTarget(ctx, t, capability, 1, 0, timeOut, 1, 0, timeOut, transmissionSchedule, responseTest) + testRemoteTarget(ctx, t, capability, 1, 0, timeOut, 4, 3, timeOut, transmissionSchedule, responseTest) + testRemoteTarget(ctx, t, capability, 1, 0, timeOut, 10, 3, timeOut, transmissionSchedule, responseTest) + + testRemoteTarget(ctx, t, capability, 4, 3, timeOut, 4, 3, timeOut, transmissionSchedule, responseTest) + testRemoteTarget(ctx, t, capability, 10, 3, timeOut, 10, 3, timeOut, transmissionSchedule, responseTest) + testRemoteTarget(ctx, t, capability, 10, 9, timeOut, 10, 9, timeOut, transmissionSchedule, responseTest) +} + +func Test_RemoteTargetCapability_CapabilityError(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + responseTest := func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error) { + require.NoError(t, responseError) + response := <-responseCh + assert.Equal(t, "failed to execute capability: an error", response.Err.Error()) + } + + capability := &TestErrorCapability{} + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + testRemoteTarget(ctx, t, capability, 10, 9, 10*time.Minute, 10, 9, 10*time.Minute, transmissionSchedule, responseTest) +} + +func Test_RemoteTargetCapability_RandomCapabilityError(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + responseTest := func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error) { + require.NoError(t, responseError) + response := <-responseCh + assert.Equal(t, "request expired", response.Err.Error()) + } + + capability := &TestRandomErrorCapability{} + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_AllAtOnce, + "deltaStage": "10ms", + }) + require.NoError(t, err) + + testRemoteTarget(ctx, t, capability, 10, 9, 10*time.Millisecond, 10, 9, 10*time.Minute, transmissionSchedule, responseTest) +} + +func testRemoteTarget(ctx context.Context, t *testing.T, underlying commoncap.TargetCapability, numWorkflowPeers int, workflowDonF uint8, workflowNodeTimeout time.Duration, + numCapabilityPeers int, capabilityDonF uint8, capabilityNodeResponseTimeout time.Duration, transmissionSchedule *values.Map, + responseTest func(t *testing.T, responseCh <-chan commoncap.CapabilityResponse, responseError error)) { + lggr := logger.TestLogger(t) + + capabilityPeers := make([]p2ptypes.PeerID, numCapabilityPeers) + for i := 0; i < numCapabilityPeers; i++ { + capabilityPeerID := p2ptypes.PeerID{} + require.NoError(t, capabilityPeerID.UnmarshalText([]byte(NewPeerID()))) + capabilityPeers[i] = capabilityPeerID + } + + capabilityPeerID := p2ptypes.PeerID{} + require.NoError(t, capabilityPeerID.UnmarshalText([]byte(NewPeerID()))) + + capDonInfo := commoncap.DON{ + ID: "capability-don", + Members: capabilityPeers, + F: capabilityDonF, + } + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Remote Target", + Version: "0.0.1", + DON: &capDonInfo, + } + + workflowPeers := make([]p2ptypes.PeerID, numWorkflowPeers) + for i := 0; i < numWorkflowPeers; i++ { + workflowPeerID := p2ptypes.PeerID{} + require.NoError(t, workflowPeerID.UnmarshalText([]byte(NewPeerID()))) + workflowPeers[i] = workflowPeerID + } + + workflowDonInfo := commoncap.DON{ + Members: workflowPeers, + ID: "workflow-don", + F: workflowDonF, + } + + broker := newTestMessageBroker() + + workflowDONs := map[string]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + capabilityNodes := make([]remotetypes.Receiver, numCapabilityPeers) + for i := 0; i < numCapabilityPeers; i++ { + capabilityPeer := capabilityPeers[i] + capabilityDispatcher := broker.NewDispatcherForNode(capabilityPeer) + capabilityNode := target.NewReceiver(ctx, lggr, capabilityPeer, underlying, capInfo, capDonInfo, workflowDONs, capabilityDispatcher, + capabilityNodeResponseTimeout) + broker.RegisterReceiverNode(capabilityPeer, capabilityNode) + capabilityNodes[i] = capabilityNode + } + + workflowNodes := make([]commoncap.TargetCapability, numWorkflowPeers) + for i := 0; i < numWorkflowPeers; i++ { + workflowPeerDispatcher := broker.NewDispatcherForNode(workflowPeers[i]) + workflowNode := target.NewClient(ctx, lggr, capInfo, workflowDonInfo, workflowPeerDispatcher, workflowNodeTimeout) + broker.RegisterReceiverNode(workflowPeers[i], workflowNode) + workflowNodes[i] = workflowNode + } + + executeInputs, err := values.NewMap( + map[string]any{ + "executeValue1": "aValue1", + }, + ) + + require.NoError(t, err) + + wg := &sync.WaitGroup{} + wg.Add(len(workflowNodes)) + + for _, caller := range workflowNodes { + go func(caller commoncap.TargetCapability) { + responseCh, err := caller.Execute(ctx, + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: "workflowID", + WorkflowExecutionID: "workflowExecutionID", + }, + Config: transmissionSchedule, + Inputs: executeInputs, + }) + + responseTest(t, responseCh, err) + wg.Done() + }(caller) + } + + wg.Wait() +} + +type testMessageBroker struct { + nodes map[p2ptypes.PeerID]remotetypes.Receiver +} + +func newTestMessageBroker() *testMessageBroker { + return &testMessageBroker{ + nodes: make(map[p2ptypes.PeerID]remotetypes.Receiver), + } +} + +func (r *testMessageBroker) NewDispatcherForNode(nodePeerID p2ptypes.PeerID) remotetypes.Dispatcher { + return &nodeDispatcher{ + callerPeerID: nodePeerID, + broker: r, + } +} + +func (r *testMessageBroker) RegisterReceiverNode(nodePeerID p2ptypes.PeerID, node remotetypes.Receiver) { + if _, ok := r.nodes[nodePeerID]; ok { + panic("node already registered") + } + + r.nodes[nodePeerID] = node +} + +func (r *testMessageBroker) Send(msg *remotetypes.MessageBody) { + receiverId := toPeerID(msg.Receiver) + + receiver, ok := r.nodes[receiverId] + if !ok { + panic("server not found for peer id") + } + + receiver.Receive(msg) +} + +func toPeerID(id []byte) p2ptypes.PeerID { + return [32]byte(id) +} + +type nodeDispatcher struct { + callerPeerID p2ptypes.PeerID + broker *testMessageBroker +} + +func (t *nodeDispatcher) Send(peerID p2ptypes.PeerID, msgBody *remotetypes.MessageBody) error { + msgBody.Version = 1 + msgBody.Sender = t.callerPeerID[:] + msgBody.Receiver = peerID[:] + msgBody.Timestamp = time.Now().UnixMilli() + t.broker.Send(msgBody) + return nil +} + +func (t *nodeDispatcher) SetReceiver(capabilityId string, donId string, receiver remotetypes.Receiver) error { + return nil +} +func (t *nodeDispatcher) RemoveReceiver(capabilityId string, donId string) {} + +type abstractTestCapability struct { +} + +func (t abstractTestCapability) Info(ctx context.Context) (commoncap.CapabilityInfo, error) { + return commoncap.CapabilityInfo{}, nil +} + +func (t abstractTestCapability) RegisterToWorkflow(ctx context.Context, request commoncap.RegisterToWorkflowRequest) error { + return nil +} + +func (t abstractTestCapability) UnregisterFromWorkflow(ctx context.Context, request commoncap.UnregisterFromWorkflowRequest) error { + return nil +} + +type TestCapability struct { + abstractTestCapability +} + +func (t TestCapability) Execute(ctx context.Context, request commoncap.CapabilityRequest) (<-chan commoncap.CapabilityResponse, error) { + ch := make(chan commoncap.CapabilityResponse, 1) + + value := request.Inputs.Underlying["executeValue1"] + + ch <- commoncap.CapabilityResponse{ + Value: value, + } + + return ch, nil +} + +type TestErrorCapability struct { + abstractTestCapability +} + +func (t TestErrorCapability) Execute(ctx context.Context, request commoncap.CapabilityRequest) (<-chan commoncap.CapabilityResponse, error) { + return nil, errors.New("an error") +} + +type TestRandomErrorCapability struct { + abstractTestCapability +} + +func (t TestRandomErrorCapability) Execute(ctx context.Context, request commoncap.CapabilityRequest) (<-chan commoncap.CapabilityResponse, error) { + return nil, errors.New(uuid.New().String()) +} + +func NewP2PPeerID(t *testing.T) p2ptypes.PeerID { + id := p2ptypes.PeerID{} + require.NoError(t, id.UnmarshalText([]byte(NewPeerID()))) + return id +} + +func NewPeerID() string { + var privKey [32]byte + _, err := rand.Read(privKey[:]) + if err != nil { + panic(err) + } + + peerID := append(libp2pMagic(), privKey[:]...) + + return base58.Encode(peerID[:]) +} + +func libp2pMagic() []byte { + return []byte{0x00, 0x24, 0x08, 0x01, 0x12, 0x20} +} diff --git a/core/capabilities/remote/target/request/client_request.go b/core/capabilities/remote/target/request/client_request.go new file mode 100644 index 00000000000..1355932c833 --- /dev/null +++ b/core/capabilities/remote/target/request/client_request.go @@ -0,0 +1,161 @@ +package request + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "sync" + "time" + + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/transmission" + "github.com/smartcontractkit/chainlink/v2/core/logger" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" +) + +type ClientRequest struct { + responseCh chan commoncap.CapabilityResponse + createdAt time.Time + responseIDCount map[[32]byte]int + errorCount map[string]int + responseReceived map[p2ptypes.PeerID]bool + + requiredIdenticalResponses int + + requestTimeout time.Duration + + respSent bool + mux sync.Mutex +} + +func NewClientRequest(ctx context.Context, lggr logger.Logger, req commoncap.CapabilityRequest, messageID string, + remoteCapabilityInfo commoncap.CapabilityInfo, localDonInfo capabilities.DON, dispatcher types.Dispatcher, + requestTimeout time.Duration) (*ClientRequest, error) { + remoteCapabilityDonInfo := remoteCapabilityInfo.DON + if remoteCapabilityDonInfo == nil { + return nil, errors.New("remote capability info missing DON") + } + + rawRequest, err := pb.MarshalCapabilityRequest(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal capability request: %w", err) + } + + tc, err := transmission.ExtractTransmissionConfig(req.Config) + if err != nil { + return nil, fmt.Errorf("failed to extract transmission config from request config: %w", err) + } + + peerIDToTransmissionDelay, err := transmission.GetPeerIDToTransmissionDelay(remoteCapabilityDonInfo.Members, localDonInfo.Config.SharedSecret, + messageID, tc) + if err != nil { + return nil, fmt.Errorf("failed to get peer ID to transmission delay: %w", err) + } + + responseReceived := make(map[p2ptypes.PeerID]bool) + for peerID, delay := range peerIDToTransmissionDelay { + responseReceived[peerID] = false + go func(peerID ragep2ptypes.PeerID, delay time.Duration) { + message := &types.MessageBody{ + CapabilityId: remoteCapabilityInfo.ID, + CapabilityDonId: remoteCapabilityDonInfo.ID, + CallerDonId: localDonInfo.ID, + Method: types.MethodExecute, + Payload: rawRequest, + MessageId: []byte(messageID), + } + + select { + case <-ctx.Done(): + return + case <-time.After(delay): + err := dispatcher.Send(peerID, message) + if err != nil { + lggr.Errorw("failed to send message", "peerID", peerID, "err", err) + } + } + }(peerID, delay) + } + + return &ClientRequest{ + createdAt: time.Now(), + requestTimeout: requestTimeout, + requiredIdenticalResponses: int(remoteCapabilityDonInfo.F + 1), + responseIDCount: make(map[[32]byte]int), + errorCount: make(map[string]int), + responseReceived: responseReceived, + responseCh: make(chan commoncap.CapabilityResponse, 1), + }, nil +} + +func (c *ClientRequest) ResponseChan() <-chan commoncap.CapabilityResponse { + return c.responseCh +} + +func (c *ClientRequest) Expired() bool { + return time.Since(c.createdAt) > c.requestTimeout +} + +func (c *ClientRequest) Cancel(err error) { + c.mux.Lock() + defer c.mux.Unlock() + if !c.respSent { + c.sendResponse(commoncap.CapabilityResponse{Err: err}) + } +} + +// TODO OnMessage assumes that only one response is received from each peer, if streaming responses need to be supported this will need to be updated +func (c *ClientRequest) OnMessage(_ context.Context, msg *types.MessageBody) error { + c.mux.Lock() + defer c.mux.Unlock() + + if msg.Sender == nil { + return fmt.Errorf("sender missing from message") + } + + sender := remote.ToPeerID(msg.Sender) + + received, expected := c.responseReceived[sender] + if !expected { + return fmt.Errorf("response from peer %s not expected", sender) + } + + if received { + return fmt.Errorf("response from peer %s already received", sender) + } + + c.responseReceived[sender] = true + + if msg.Error == types.Error_OK { + responseID := sha256.Sum256(msg.Payload) + c.responseIDCount[responseID]++ + + if c.responseIDCount[responseID] == c.requiredIdenticalResponses { + capabilityResponse, err := pb.UnmarshalCapabilityResponse(msg.Payload) + if err != nil { + c.sendResponse(commoncap.CapabilityResponse{Err: fmt.Errorf("failed to unmarshal capability response: %w", err)}) + } else { + c.sendResponse(commoncap.CapabilityResponse{Value: capabilityResponse.Value}) + } + } + } else { + c.errorCount[msg.ErrorMsg]++ + if c.errorCount[msg.ErrorMsg] == c.requiredIdenticalResponses { + c.sendResponse(commoncap.CapabilityResponse{Err: errors.New(msg.ErrorMsg)}) + } + } + return nil +} + +func (c *ClientRequest) sendResponse(response commoncap.CapabilityResponse) { + c.responseCh <- response + close(c.responseCh) + c.respSent = true +} diff --git a/core/capabilities/remote/target/request/client_request_test.go b/core/capabilities/remote/target/request/client_request_test.go new file mode 100644 index 00000000000..930ba595625 --- /dev/null +++ b/core/capabilities/remote/target/request/client_request_test.go @@ -0,0 +1,311 @@ +package request_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target/request" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/transmission" + "github.com/smartcontractkit/chainlink/v2/core/logger" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" +) + +func Test_ClientRequest_MessageValidation(t *testing.T) { + lggr := logger.TestLogger(t) + + numCapabilityPeers := 2 + capabilityPeers := make([]p2ptypes.PeerID, numCapabilityPeers) + for i := 0; i < numCapabilityPeers; i++ { + capabilityPeers[i] = NewP2PPeerID(t) + } + + capDonInfo := commoncap.DON{ + ID: "capability-don", + Members: capabilityPeers, + F: 1, + } + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Remote Target", + Version: "0.0.1", + DON: &capDonInfo, + } + + numWorkflowPeers := 2 + workflowPeers := make([]p2ptypes.PeerID, numWorkflowPeers) + for i := 0; i < numWorkflowPeers; i++ { + workflowPeers[i] = NewP2PPeerID(t) + } + + workflowDonInfo := commoncap.DON{ + Members: workflowPeers, + ID: "workflow-don", + } + + executeInputs, err := values.NewMap( + map[string]any{ + "executeValue1": "aValue1", + }, + ) + require.NoError(t, err) + + transmissionSchedule, err := values.NewMap(map[string]any{ + "schedule": transmission.Schedule_OneAtATime, + "deltaStage": "1000ms", + }) + require.NoError(t, err) + + capabilityRequest := commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: "workflowID", + WorkflowExecutionID: "workflowExecutionID", + }, + Inputs: executeInputs, + Config: transmissionSchedule, + } + + capabilityResponse := commoncap.CapabilityResponse{ + Value: values.NewString("response1"), + Err: nil, + } + + rawResponse, err := pb.MarshalCapabilityResponse(capabilityResponse) + require.NoError(t, err) + + messageID, err := target.GetMessageIDForRequest(capabilityRequest) + require.NoError(t, err) + + msg := &types.MessageBody{ + CapabilityId: capInfo.ID, + CapabilityDonId: capDonInfo.ID, + CallerDonId: workflowDonInfo.ID, + Method: types.MethodExecute, + Payload: rawResponse, + MessageId: []byte("messageID"), + } + + t.Run("Send second message with different response", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + dispatcher := &clientRequestTestDispatcher{msgs: make(chan *types.MessageBody, 100)} + request, err := request.NewClientRequest(ctx, lggr, capabilityRequest, messageID, capInfo, + workflowDonInfo, dispatcher, 10*time.Minute) + require.NoError(t, err) + + capabilityResponse2 := commoncap.CapabilityResponse{ + Value: values.NewString("response2"), + Err: nil, + } + + rawResponse2, err := pb.MarshalCapabilityResponse(capabilityResponse2) + require.NoError(t, err) + msg2 := &types.MessageBody{ + CapabilityId: capInfo.ID, + CapabilityDonId: capDonInfo.ID, + CallerDonId: workflowDonInfo.ID, + Method: types.MethodExecute, + Payload: rawResponse2, + MessageId: []byte("messageID"), + } + + msg.Sender = capabilityPeers[0][:] + err = request.OnMessage(ctx, msg) + require.NoError(t, err) + + msg2.Sender = capabilityPeers[1][:] + err = request.OnMessage(ctx, msg2) + require.NoError(t, err) + + select { + case <-request.ResponseChan(): + t.Fatal("expected no response") + default: + } + }) + + t.Run("Send second message from non calling Don peer", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + dispatcher := &clientRequestTestDispatcher{msgs: make(chan *types.MessageBody, 100)} + request, err := request.NewClientRequest(ctx, lggr, capabilityRequest, messageID, capInfo, + workflowDonInfo, dispatcher, 10*time.Minute) + require.NoError(t, err) + + msg.Sender = capabilityPeers[0][:] + err = request.OnMessage(ctx, msg) + require.NoError(t, err) + + nonDonPeer := NewP2PPeerID(t) + msg.Sender = nonDonPeer[:] + err = request.OnMessage(ctx, msg) + require.NotNil(t, err) + + select { + case <-request.ResponseChan(): + t.Fatal("expected no response") + default: + } + }) + + t.Run("Send second message from same peer as first message", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + dispatcher := &clientRequestTestDispatcher{msgs: make(chan *types.MessageBody, 100)} + request, err := request.NewClientRequest(ctx, lggr, capabilityRequest, messageID, capInfo, + workflowDonInfo, dispatcher, 10*time.Minute) + require.NoError(t, err) + + msg.Sender = capabilityPeers[0][:] + err = request.OnMessage(ctx, msg) + require.NoError(t, err) + err = request.OnMessage(ctx, msg) + require.NotNil(t, err) + + select { + case <-request.ResponseChan(): + t.Fatal("expected no response") + default: + } + }) + + t.Run("Send second message with same error as first", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + dispatcher := &clientRequestTestDispatcher{msgs: make(chan *types.MessageBody, 100)} + request, err := request.NewClientRequest(ctx, lggr, capabilityRequest, messageID, capInfo, + workflowDonInfo, dispatcher, 10*time.Minute) + require.NoError(t, err) + + <-dispatcher.msgs + <-dispatcher.msgs + assert.Equal(t, 0, len(dispatcher.msgs)) + + msgWithError := &types.MessageBody{ + CapabilityId: capInfo.ID, + CapabilityDonId: capDonInfo.ID, + CallerDonId: workflowDonInfo.ID, + Method: types.MethodExecute, + Payload: rawResponse, + MessageId: []byte("messageID"), + Error: types.Error_INTERNAL_ERROR, + ErrorMsg: "an error", + } + + msgWithError.Sender = capabilityPeers[0][:] + err = request.OnMessage(ctx, msgWithError) + require.NoError(t, err) + + msgWithError.Sender = capabilityPeers[1][:] + err = request.OnMessage(ctx, msgWithError) + require.NoError(t, err) + + response := <-request.ResponseChan() + + assert.Equal(t, "an error", response.Err.Error()) + }) + + t.Run("Send second message with different error to first", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + dispatcher := &clientRequestTestDispatcher{msgs: make(chan *types.MessageBody, 100)} + request, err := request.NewClientRequest(ctx, lggr, capabilityRequest, messageID, capInfo, + workflowDonInfo, dispatcher, 10*time.Minute) + require.NoError(t, err) + + <-dispatcher.msgs + <-dispatcher.msgs + assert.Equal(t, 0, len(dispatcher.msgs)) + + msgWithError := &types.MessageBody{ + CapabilityId: capInfo.ID, + CapabilityDonId: capDonInfo.ID, + CallerDonId: workflowDonInfo.ID, + Method: types.MethodExecute, + Payload: rawResponse, + MessageId: []byte("messageID"), + Error: types.Error_INTERNAL_ERROR, + ErrorMsg: "an error", + Sender: capabilityPeers[0][:], + } + + msgWithError2 := &types.MessageBody{ + CapabilityId: capInfo.ID, + CapabilityDonId: capDonInfo.ID, + CallerDonId: workflowDonInfo.ID, + Method: types.MethodExecute, + Payload: rawResponse, + MessageId: []byte("messageID"), + Error: types.Error_INTERNAL_ERROR, + ErrorMsg: "an error2", + Sender: capabilityPeers[1][:], + } + + err = request.OnMessage(ctx, msgWithError) + require.NoError(t, err) + err = request.OnMessage(ctx, msgWithError2) + require.NoError(t, err) + + select { + case <-request.ResponseChan(): + t.Fatal("expected no response") + default: + } + }) + + t.Run("Send second valid message", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + dispatcher := &clientRequestTestDispatcher{msgs: make(chan *types.MessageBody, 100)} + request, err := request.NewClientRequest(ctx, lggr, capabilityRequest, messageID, capInfo, + workflowDonInfo, dispatcher, 10*time.Minute) + require.NoError(t, err) + + <-dispatcher.msgs + <-dispatcher.msgs + assert.Equal(t, 0, len(dispatcher.msgs)) + + msg.Sender = capabilityPeers[0][:] + err = request.OnMessage(ctx, msg) + require.NoError(t, err) + + msg.Sender = capabilityPeers[1][:] + err = request.OnMessage(ctx, msg) + require.NoError(t, err) + + response := <-request.ResponseChan() + + assert.Equal(t, response.Value, values.NewString("response1")) + }) +} + +type clientRequestTestDispatcher struct { + msgs chan *types.MessageBody +} + +func (t *clientRequestTestDispatcher) SetReceiver(capabilityId string, donId string, receiver types.Receiver) error { + return nil +} + +func (t *clientRequestTestDispatcher) RemoveReceiver(capabilityId string, donId string) {} + +func (t *clientRequestTestDispatcher) Send(peerID p2ptypes.PeerID, msgBody *types.MessageBody) error { + t.msgs <- msgBody + return nil +} diff --git a/core/capabilities/remote/target/request/server_request.go b/core/capabilities/remote/target/request/server_request.go new file mode 100644 index 00000000000..84968de9f11 --- /dev/null +++ b/core/capabilities/remote/target/request/server_request.go @@ -0,0 +1,222 @@ +package request + +import ( + "context" + "fmt" + "sync" + "time" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" +) + +type response struct { + response []byte + error types.Error + errorMsg string +} + +type ServerRequest struct { + capability capabilities.TargetCapability + + capabilityPeerId p2ptypes.PeerID + capabilityID string + capabilityDonID string + + dispatcher types.Dispatcher + + requesters map[p2ptypes.PeerID]bool + responseSentToRequester map[p2ptypes.PeerID]bool + + createdTime time.Time + + response *response + + callingDon commoncap.DON + + requestMessageID string + requestTimeout time.Duration + + mux sync.Mutex +} + +func NewServerRequest(capability capabilities.TargetCapability, capabilityID string, capabilityDonID string, capabilityPeerId p2ptypes.PeerID, + callingDon commoncap.DON, requestMessageID string, + dispatcher types.Dispatcher, requestTimeout time.Duration) *ServerRequest { + return &ServerRequest{ + capability: capability, + createdTime: time.Now(), + capabilityID: capabilityID, + capabilityDonID: capabilityDonID, + capabilityPeerId: capabilityPeerId, + dispatcher: dispatcher, + requesters: map[p2ptypes.PeerID]bool{}, + responseSentToRequester: map[p2ptypes.PeerID]bool{}, + callingDon: callingDon, + requestMessageID: requestMessageID, + requestTimeout: requestTimeout, + } +} + +func (e *ServerRequest) OnMessage(ctx context.Context, msg *types.MessageBody) error { + e.mux.Lock() + defer e.mux.Unlock() + + if msg.Sender == nil { + return fmt.Errorf("sender missing from message") + } + + requester := remote.ToPeerID(msg.Sender) + if err := e.addRequester(requester); err != nil { + return fmt.Errorf("failed to add requester to request: %w", err) + } + + if e.minimumRequiredRequestsReceived() && !e.hasResponse() { + if err := e.executeRequest(ctx, msg.Payload); err != nil { + e.setError(types.Error_INTERNAL_ERROR, err.Error()) + } + } + + if err := e.sendResponses(); err != nil { + return fmt.Errorf("failed to send responses: %w", err) + } + + return nil +} + +func (e *ServerRequest) Expired() bool { + return time.Since(e.createdTime) > e.requestTimeout +} + +func (e *ServerRequest) Cancel(err types.Error, msg string) error { + e.mux.Lock() + defer e.mux.Unlock() + + if e.hasResponse() { + return fmt.Errorf("request already has response") + } + + e.setError(err, msg) + if err := e.sendResponses(); err != nil { + return fmt.Errorf("failed to send responses: %w", err) + } + + return nil +} + +func (e *ServerRequest) executeRequest(ctx context.Context, payload []byte) error { + ctxWithTimeout, cancel := context.WithTimeout(ctx, e.requestTimeout) + defer cancel() + + capabilityRequest, err := pb.UnmarshalCapabilityRequest(payload) + if err != nil { + return fmt.Errorf("failed to unmarshal capability request: %w", err) + } + + capResponseCh, err := e.capability.Execute(ctxWithTimeout, capabilityRequest) + + if err != nil { + return fmt.Errorf("failed to execute capability: %w", err) + } + + // TODO working on the assumption that the capability will only ever return one response from its channel (for now at least) + capResponse := <-capResponseCh + responsePayload, err := pb.MarshalCapabilityResponse(capResponse) + if err != nil { + return fmt.Errorf("failed to marshal capability response: %w", err) + } + + e.setResult(responsePayload) + + return nil +} + +func (e *ServerRequest) addRequester(from p2ptypes.PeerID) error { + fromPeerInCallingDon := false + for _, member := range e.callingDon.Members { + if member == from { + fromPeerInCallingDon = true + break + } + } + + if !fromPeerInCallingDon { + return fmt.Errorf("request received from peer %s not in calling don", from) + } + + if e.requesters[from] { + return fmt.Errorf("request already received from peer %s", from) + } + + e.requesters[from] = true + + return nil +} + +func (e *ServerRequest) minimumRequiredRequestsReceived() bool { + return len(e.requesters) >= int(e.callingDon.F+1) +} + +func (e *ServerRequest) setResult(result []byte) { + e.response = &response{ + response: result, + } +} + +func (e *ServerRequest) setError(err types.Error, errMsg string) { + e.response = &response{ + error: err, + errorMsg: errMsg, + } +} + +func (e *ServerRequest) hasResponse() bool { + return e.response != nil +} + +func (e *ServerRequest) sendResponses() error { + if e.hasResponse() { + for requester := range e.requesters { + if !e.responseSentToRequester[requester] { + e.responseSentToRequester[requester] = true + if err := e.sendResponse(requester); err != nil { + return fmt.Errorf("failed to send response to requester %s: %w", requester, err) + } + } + } + } + + return nil +} + +func (e *ServerRequest) sendResponse(requester p2ptypes.PeerID) error { + responseMsg := types.MessageBody{ + CapabilityId: e.capabilityID, + CapabilityDonId: e.capabilityDonID, + CallerDonId: e.callingDon.ID, + Method: types.MethodExecute, + MessageId: []byte(e.requestMessageID), + Sender: e.capabilityPeerId[:], + Receiver: requester[:], + } + + if e.response.error != types.Error_OK { + responseMsg.Error = e.response.error + responseMsg.ErrorMsg = e.response.errorMsg + } else { + responseMsg.Payload = e.response.response + } + + if err := e.dispatcher.Send(requester, &responseMsg); err != nil { + return fmt.Errorf("failed to send response to dispatcher: %w", err) + } + + e.responseSentToRequester[requester] = true + + return nil +} diff --git a/core/capabilities/remote/target/request/server_request_test.go b/core/capabilities/remote/target/request/server_request_test.go new file mode 100644 index 00000000000..0529a1e9004 --- /dev/null +++ b/core/capabilities/remote/target/request/server_request_test.go @@ -0,0 +1,261 @@ +package request_test + +import ( + "context" + "crypto/rand" + "errors" + "testing" + "time" + + "github.com/mr-tron/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target/request" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" +) + +func Test_ServerRequest_MessageValidation(t *testing.T) { + capability := TestCapability{} + capabilityPeerID := NewP2PPeerID(t) + + numWorkflowPeers := 2 + workflowPeers := make([]p2ptypes.PeerID, numWorkflowPeers) + for i := 0; i < numWorkflowPeers; i++ { + workflowPeers[i] = NewP2PPeerID(t) + } + + callingDon := commoncap.DON{ + Members: workflowPeers, + ID: "workflow-don", + F: 1, + } + + dispatcher := &testDispatcher{} + + executeInputs, err := values.NewMap( + map[string]any{ + "executeValue1": "aValue1", + }, + ) + require.NoError(t, err) + + capabilityRequest := commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: "workflowID", + WorkflowExecutionID: "workflowExecutionID", + }, + Inputs: executeInputs, + } + + rawRequest, err := pb.MarshalCapabilityRequest(capabilityRequest) + require.NoError(t, err) + + t.Run("Send duplicate message", func(t *testing.T) { + req := request.NewServerRequest(capability, "capabilityID", "capabilityDonID", + capabilityPeerID, callingDon, "requestMessageID", dispatcher, 10*time.Minute) + + err := sendValidRequest(req, workflowPeers, capabilityPeerID, rawRequest) + require.NoError(t, err) + err = sendValidRequest(req, workflowPeers, capabilityPeerID, rawRequest) + assert.NotNil(t, err) + }) + + t.Run("Send message with non calling don peer", func(t *testing.T) { + req := request.NewServerRequest(capability, "capabilityID", "capabilityDonID", + capabilityPeerID, callingDon, "requestMessageID", dispatcher, 10*time.Minute) + + err := sendValidRequest(req, workflowPeers, capabilityPeerID, rawRequest) + require.NoError(t, err) + + nonDonPeer := NewP2PPeerID(t) + err = req.OnMessage(context.Background(), &types.MessageBody{ + Version: 0, + Sender: nonDonPeer[:], + Receiver: capabilityPeerID[:], + MessageId: []byte("workflowID" + "workflowExecutionID"), + CapabilityId: "capabilityID", + CapabilityDonId: "capabilityDonID", + CallerDonId: "workflow-don", + Method: types.MethodExecute, + Payload: rawRequest, + }) + + assert.NotNil(t, err) + }) + + t.Run("Send message invalid payload", func(t *testing.T) { + req := request.NewServerRequest(capability, "capabilityID", "capabilityDonID", + capabilityPeerID, callingDon, "requestMessageID", dispatcher, 10*time.Minute) + + err := sendValidRequest(req, workflowPeers, capabilityPeerID, rawRequest) + require.NoError(t, err) + + err = req.OnMessage(context.Background(), &types.MessageBody{ + Version: 0, + Sender: workflowPeers[1][:], + Receiver: capabilityPeerID[:], + MessageId: []byte("workflowID" + "workflowExecutionID"), + CapabilityId: "capabilityID", + CapabilityDonId: "capabilityDonID", + CallerDonId: "workflow-don", + Method: types.MethodExecute, + Payload: append(rawRequest, []byte("asdf")...), + }) + assert.NoError(t, err) + assert.Equal(t, 2, len(dispatcher.msgs)) + assert.Equal(t, dispatcher.msgs[0].Error, types.Error_INTERNAL_ERROR) + assert.Equal(t, dispatcher.msgs[1].Error, types.Error_INTERNAL_ERROR) + }) + + t.Run("Send second valid request when capability errors", func(t *testing.T) { + dispatcher := &testDispatcher{} + req := request.NewServerRequest(TestErrorCapability{}, "capabilityID", "capabilityDonID", + capabilityPeerID, callingDon, "requestMessageID", dispatcher, 10*time.Minute) + + err := sendValidRequest(req, workflowPeers, capabilityPeerID, rawRequest) + require.NoError(t, err) + + err = req.OnMessage(context.Background(), &types.MessageBody{ + Version: 0, + Sender: workflowPeers[1][:], + Receiver: capabilityPeerID[:], + MessageId: []byte("workflowID" + "workflowExecutionID"), + CapabilityId: "capabilityID", + CapabilityDonId: "capabilityDonID", + CallerDonId: "workflow-don", + Method: types.MethodExecute, + Payload: rawRequest, + }) + assert.NoError(t, err) + assert.Equal(t, 2, len(dispatcher.msgs)) + assert.Equal(t, dispatcher.msgs[0].Error, types.Error_INTERNAL_ERROR) + assert.Equal(t, dispatcher.msgs[0].ErrorMsg, "failed to execute capability: an error") + assert.Equal(t, dispatcher.msgs[1].Error, types.Error_INTERNAL_ERROR) + assert.Equal(t, dispatcher.msgs[1].ErrorMsg, "failed to execute capability: an error") + }) + + t.Run("Send second valid request", func(t *testing.T) { + dispatcher := &testDispatcher{} + request := request.NewServerRequest(capability, "capabilityID", "capabilityDonID", + capabilityPeerID, callingDon, "requestMessageID", dispatcher, 10*time.Minute) + + err := sendValidRequest(request, workflowPeers, capabilityPeerID, rawRequest) + require.NoError(t, err) + + err = request.OnMessage(context.Background(), &types.MessageBody{ + Version: 0, + Sender: workflowPeers[1][:], + Receiver: capabilityPeerID[:], + MessageId: []byte("workflowID" + "workflowExecutionID"), + CapabilityId: "capabilityID", + CapabilityDonId: "capabilityDonID", + CallerDonId: "workflow-don", + Method: types.MethodExecute, + Payload: rawRequest, + }) + assert.NoError(t, err) + assert.Equal(t, 2, len(dispatcher.msgs)) + assert.Equal(t, dispatcher.msgs[0].Error, types.Error_OK) + assert.Equal(t, dispatcher.msgs[1].Error, types.Error_OK) + }) +} + +type serverRequest interface { + OnMessage(ctx context.Context, msg *types.MessageBody) error +} + +func sendValidRequest(request serverRequest, workflowPeers []p2ptypes.PeerID, capabilityPeerID p2ptypes.PeerID, + rawRequest []byte) error { + return request.OnMessage(context.Background(), &types.MessageBody{ + Version: 0, + Sender: workflowPeers[0][:], + Receiver: capabilityPeerID[:], + MessageId: []byte("workflowID" + "workflowExecutionID"), + CapabilityId: "capabilityID", + CapabilityDonId: "capabilityDonID", + CallerDonId: "workflow-don", + Method: types.MethodExecute, + Payload: rawRequest, + }) +} + +type testDispatcher struct { + msgs []*types.MessageBody +} + +func (t *testDispatcher) SetReceiver(capabilityId string, donId string, receiver types.Receiver) error { + return nil +} + +func (t *testDispatcher) RemoveReceiver(capabilityId string, donId string) {} + +func (t *testDispatcher) Send(peerID p2ptypes.PeerID, msgBody *types.MessageBody) error { + t.msgs = append(t.msgs, msgBody) + return nil +} + +type abstractTestCapability struct { +} + +func (t abstractTestCapability) Info(ctx context.Context) (commoncap.CapabilityInfo, error) { + return commoncap.CapabilityInfo{}, nil +} + +func (t abstractTestCapability) RegisterToWorkflow(ctx context.Context, request commoncap.RegisterToWorkflowRequest) error { + return nil +} + +func (t abstractTestCapability) UnregisterFromWorkflow(ctx context.Context, request commoncap.UnregisterFromWorkflowRequest) error { + return nil +} + +type TestCapability struct { + abstractTestCapability +} + +func (t TestCapability) Execute(ctx context.Context, request commoncap.CapabilityRequest) (<-chan commoncap.CapabilityResponse, error) { + ch := make(chan commoncap.CapabilityResponse, 1) + + value := request.Inputs.Underlying["executeValue1"] + + ch <- commoncap.CapabilityResponse{ + Value: value, + } + + return ch, nil +} + +type TestErrorCapability struct { + abstractTestCapability +} + +func (t TestErrorCapability) Execute(ctx context.Context, request commoncap.CapabilityRequest) (<-chan commoncap.CapabilityResponse, error) { + return nil, errors.New("an error") +} + +func NewP2PPeerID(t *testing.T) p2ptypes.PeerID { + id := p2ptypes.PeerID{} + require.NoError(t, id.UnmarshalText([]byte(NewPeerID()))) + return id +} + +func NewPeerID() string { + var privKey [32]byte + _, err := rand.Read(privKey[:]) + if err != nil { + panic(err) + } + + peerID := append(libp2pMagic(), privKey[:]...) + + return base58.Encode(peerID[:]) +} + +func libp2pMagic() []byte { + return []byte{0x00, 0x24, 0x08, 0x01, 0x12, 0x20} +} diff --git a/core/capabilities/remote/target/server.go b/core/capabilities/remote/target/server.go new file mode 100644 index 00000000000..bb3bc6e4edc --- /dev/null +++ b/core/capabilities/remote/target/server.go @@ -0,0 +1,131 @@ +package target + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "sync" + "time" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target/request" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +// server manages all external users of a local target capability. +// Its responsibilities are: +// 1. Manage requests from external nodes executing the target capability once sufficient requests are received. +// 2. Send out responses produced by an underlying capability to all requesters. +// +// server communicates with corresponding client on remote nodes. +type server struct { + lggr logger.Logger + peerID p2ptypes.PeerID + underlying commoncap.TargetCapability + capInfo commoncap.CapabilityInfo + localDonInfo capabilities.DON + workflowDONs map[string]commoncap.DON + dispatcher types.Dispatcher + + requestIDToRequest map[string]*request.ServerRequest + requestTimeout time.Duration + + receiveLock sync.Mutex +} + +var _ types.Receiver = &server{} + +func NewReceiver(ctx context.Context, lggr logger.Logger, peerID p2ptypes.PeerID, underlying commoncap.TargetCapability, capInfo commoncap.CapabilityInfo, localDonInfo capabilities.DON, + workflowDONs map[string]commoncap.DON, dispatcher types.Dispatcher, requestTimeout time.Duration) *server { + r := &server{ + underlying: underlying, + peerID: peerID, + capInfo: capInfo, + localDonInfo: localDonInfo, + workflowDONs: workflowDONs, + dispatcher: dispatcher, + + requestIDToRequest: map[string]*request.ServerRequest{}, + requestTimeout: requestTimeout, + + lggr: lggr, + } + + go func() { + ticker := time.NewTicker(requestTimeout) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + r.expireRequests() + } + } + }() + + return r +} + +func (r *server) expireRequests() { + r.receiveLock.Lock() + defer r.receiveLock.Unlock() + + for requestID, executeReq := range r.requestIDToRequest { + if executeReq.Expired() { + err := executeReq.Cancel(types.Error_TIMEOUT, "request expired") + if err != nil { + r.lggr.Errorw("failed to cancel request", "request", executeReq, "err", err) + } + delete(r.requestIDToRequest, requestID) + } + } +} + +// Receive handles incoming messages from remote nodes and dispatches them to the corresponding request without blocking +// the client. +func (r *server) Receive(msg *types.MessageBody) { + r.receiveLock.Lock() + defer r.receiveLock.Unlock() + // TODO should the dispatcher be passing in a context? + ctx := context.Background() + + if msg.Method != types.MethodExecute { + r.lggr.Errorw("received request for unsupported method type", "method", msg.Method) + return + } + + // A request is uniquely identified by the message id and the hash of the payload to prevent a malicious + // actor from sending a different payload with the same message id + messageId := GetMessageID(msg) + hash := sha256.Sum256(msg.Payload) + requestID := messageId + hex.EncodeToString(hash[:]) + + if _, ok := r.requestIDToRequest[requestID]; !ok { + callingDon, ok := r.workflowDONs[msg.CallerDonId] + if !ok { + r.lggr.Errorw("received request from unregistered don", "donId", msg.CallerDonId) + return + } + + r.requestIDToRequest[requestID] = request.NewServerRequest(r.underlying, r.capInfo.ID, r.localDonInfo.ID, r.peerID, + callingDon, messageId, r.dispatcher, r.requestTimeout) + } + + req := r.requestIDToRequest[requestID] + + go func() { + err := req.OnMessage(ctx, msg) + if err != nil { + r.lggr.Errorw("request failed to OnMessage new message", "request", req, "err", err) + } + }() +} + +func GetMessageID(msg *types.MessageBody) string { + return string(msg.MessageId) +} diff --git a/core/capabilities/remote/target/server_test.go b/core/capabilities/remote/target/server_test.go new file mode 100644 index 00000000000..ed80e760951 --- /dev/null +++ b/core/capabilities/remote/target/server_test.go @@ -0,0 +1,224 @@ +package target_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/target" + remotetypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" +) + +func Test_Server_RespondsAfterSufficientRequests(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + numCapabilityPeers := 4 + + callers := testRemoteTargetServer(ctx, t, &TestCapability{}, 10, 9, numCapabilityPeers, 3, 10*time.Minute) + + for _, caller := range callers { + _, err := caller.Execute(context.Background(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: "workflowID", + WorkflowExecutionID: "workflowExecutionID", + }, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + for i := 0; i < numCapabilityPeers; i++ { + msg := <-caller.receivedMessages + assert.Equal(t, remotetypes.Error_OK, msg.Error) + } + } +} + +func Test_Server_InsufficientCallers(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + numCapabilityPeers := 4 + + callers := testRemoteTargetServer(ctx, t, &TestCapability{}, 10, 10, numCapabilityPeers, 3, 100*time.Millisecond) + + for _, caller := range callers { + _, err := caller.Execute(context.Background(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: "workflowID", + WorkflowExecutionID: "workflowExecutionID", + }, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + for i := 0; i < numCapabilityPeers; i++ { + msg := <-caller.receivedMessages + assert.Equal(t, remotetypes.Error_TIMEOUT, msg.Error) + } + } +} + +func Test_Server_CapabilityError(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + defer cancel() + + numCapabilityPeers := 4 + + callers := testRemoteTargetServer(ctx, t, &TestErrorCapability{}, 10, 9, numCapabilityPeers, 3, 100*time.Millisecond) + + for _, caller := range callers { + _, err := caller.Execute(context.Background(), + commoncap.CapabilityRequest{ + Metadata: commoncap.RequestMetadata{ + WorkflowID: "workflowID", + WorkflowExecutionID: "workflowExecutionID", + }, + }) + require.NoError(t, err) + } + + for _, caller := range callers { + for i := 0; i < numCapabilityPeers; i++ { + msg := <-caller.receivedMessages + assert.Equal(t, remotetypes.Error_INTERNAL_ERROR, msg.Error) + } + } +} + +func testRemoteTargetServer(ctx context.Context, t *testing.T, + underlying commoncap.TargetCapability, + numWorkflowPeers int, workflowDonF uint8, + numCapabilityPeers int, capabilityDonF uint8, capabilityNodeResponseTimeout time.Duration) []*serverTestClient { + lggr := logger.TestLogger(t) + + capabilityPeers := make([]p2ptypes.PeerID, numCapabilityPeers) + for i := 0; i < numCapabilityPeers; i++ { + capabilityPeerID := NewP2PPeerID(t) + capabilityPeers[i] = capabilityPeerID + } + + capDonInfo := commoncap.DON{ + ID: "capability-don", + Members: capabilityPeers, + F: capabilityDonF, + } + + capInfo := commoncap.CapabilityInfo{ + ID: "cap_id", + CapabilityType: commoncap.CapabilityTypeTarget, + Description: "Remote Target", + Version: "0.0.1", + DON: &capDonInfo, + } + + workflowPeers := make([]p2ptypes.PeerID, numWorkflowPeers) + for i := 0; i < numWorkflowPeers; i++ { + workflowPeers[i] = NewP2PPeerID(t) + } + + workflowDonInfo := commoncap.DON{ + Members: workflowPeers, + ID: "workflow-don", + F: workflowDonF, + } + + broker := newTestMessageBroker() + + workflowDONs := map[string]commoncap.DON{ + workflowDonInfo.ID: workflowDonInfo, + } + + capabilityNodes := make([]remotetypes.Receiver, numCapabilityPeers) + for i := 0; i < numCapabilityPeers; i++ { + capabilityPeer := capabilityPeers[i] + capabilityDispatcher := broker.NewDispatcherForNode(capabilityPeer) + capabilityNode := target.NewReceiver(ctx, lggr, capabilityPeer, underlying, capInfo, capDonInfo, workflowDONs, capabilityDispatcher, + capabilityNodeResponseTimeout) + broker.RegisterReceiverNode(capabilityPeer, capabilityNode) + capabilityNodes[i] = capabilityNode + } + + workflowNodes := make([]*serverTestClient, numWorkflowPeers) + for i := 0; i < numWorkflowPeers; i++ { + workflowPeerDispatcher := broker.NewDispatcherForNode(workflowPeers[i]) + workflowNode := newServerTestClient(workflowPeers[i], capDonInfo, workflowPeerDispatcher) + broker.RegisterReceiverNode(workflowPeers[i], workflowNode) + workflowNodes[i] = workflowNode + } + + return workflowNodes +} + +type serverTestClient struct { + peerID p2ptypes.PeerID + dispatcher remotetypes.Dispatcher + capabilityDonInfo commoncap.DON + receivedMessages chan *remotetypes.MessageBody + callerDonID string +} + +func (r *serverTestClient) Receive(msg *remotetypes.MessageBody) { + r.receivedMessages <- msg +} + +func newServerTestClient(peerID p2ptypes.PeerID, capabilityDonInfo commoncap.DON, + dispatcher remotetypes.Dispatcher) *serverTestClient { + return &serverTestClient{peerID: peerID, dispatcher: dispatcher, capabilityDonInfo: capabilityDonInfo, + receivedMessages: make(chan *remotetypes.MessageBody, 100), callerDonID: "workflow-don"} +} + +func (r *serverTestClient) Info(ctx context.Context) (commoncap.CapabilityInfo, error) { + panic("not implemented") +} + +func (r *serverTestClient) RegisterToWorkflow(ctx context.Context, request commoncap.RegisterToWorkflowRequest) error { + panic("not implemented") +} + +func (r *serverTestClient) UnregisterFromWorkflow(ctx context.Context, request commoncap.UnregisterFromWorkflowRequest) error { + panic("not implemented") +} + +func (r *serverTestClient) Execute(ctx context.Context, req commoncap.CapabilityRequest) (<-chan commoncap.CapabilityResponse, error) { + rawRequest, err := pb.MarshalCapabilityRequest(req) + if err != nil { + return nil, err + } + + messageID, err := target.GetMessageIDForRequest(req) + if err != nil { + return nil, err + } + + for _, node := range r.capabilityDonInfo.Members { + message := &remotetypes.MessageBody{ + CapabilityId: "capability-id", + CapabilityDonId: "capability-don", + CallerDonId: "workflow-don", + Method: remotetypes.MethodExecute, + Payload: rawRequest, + MessageId: []byte(messageID), + Sender: r.peerID[:], + Receiver: node[:], + } + + if err = r.dispatcher.Send(node, message); err != nil { + return nil, err + } + } + + return nil, nil +} diff --git a/core/capabilities/remote/target_test.go b/core/capabilities/remote/target_test.go deleted file mode 100644 index 0f9bad51f67..00000000000 --- a/core/capabilities/remote/target_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package remote_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote" - remoteMocks "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types/mocks" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" - "github.com/smartcontractkit/chainlink/v2/core/logger" - p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" -) - -func TestTarget_Placeholder(t *testing.T) { - lggr := logger.TestLogger(t) - ctx := testutils.Context(t) - donInfo := &capabilities.DON{ - Members: []p2ptypes.PeerID{{}}, - } - dispatcher := remoteMocks.NewDispatcher(t) - dispatcher.On("Send", mock.Anything, mock.Anything).Return(nil) - target := remote.NewRemoteTargetCaller(commoncap.CapabilityInfo{}, donInfo, dispatcher, lggr) - - _, err := target.Execute(ctx, commoncap.CapabilityRequest{}) - assert.NoError(t, err) -} diff --git a/core/capabilities/remote/types/message.pb.go b/core/capabilities/remote/types/message.pb.go index d8e9579e96c..7cef9d45748 100644 --- a/core/capabilities/remote/types/message.pb.go +++ b/core/capabilities/remote/types/message.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 -// protoc v3.21.8 +// protoc-gen-go v1.33.0 +// protoc v4.25.1 // source: core/capabilities/remote/types/message.proto package types @@ -26,6 +26,9 @@ const ( Error_OK Error = 0 Error_VALIDATION_FAILED Error = 1 Error_CAPABILITY_NOT_FOUND Error = 2 + Error_INVALID_REQUEST Error = 3 + Error_TIMEOUT Error = 4 + Error_INTERNAL_ERROR Error = 5 ) // Enum value maps for Error. @@ -34,11 +37,17 @@ var ( 0: "OK", 1: "VALIDATION_FAILED", 2: "CAPABILITY_NOT_FOUND", + 3: "INVALID_REQUEST", + 4: "TIMEOUT", + 5: "INTERNAL_ERROR", } Error_value = map[string]int32{ "OK": 0, "VALIDATION_FAILED": 1, "CAPABILITY_NOT_FOUND": 2, + "INVALID_REQUEST": 3, + "TIMEOUT": 4, + "INTERNAL_ERROR": 5, } ) @@ -129,7 +138,6 @@ type MessageBody struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // header fields set and validated by the Dispatcher Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` Sender []byte `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"` Receiver []byte `protobuf:"bytes,3,opt,name=receiver,proto3" json:"receiver,omitempty"` @@ -140,9 +148,11 @@ type MessageBody struct { CallerDonId string `protobuf:"bytes,8,opt,name=caller_don_id,json=callerDonId,proto3" json:"caller_don_id,omitempty"` Method string `protobuf:"bytes,9,opt,name=method,proto3" json:"method,omitempty"` Error Error `protobuf:"varint,10,opt,name=error,proto3,enum=remote.Error" json:"error,omitempty"` + ErrorMsg string `protobuf:"bytes,11,opt,name=errorMsg,proto3" json:"errorMsg,omitempty"` // payload contains a CapabilityRequest or CapabilityResponse - Payload []byte `protobuf:"bytes,11,opt,name=payload,proto3" json:"payload,omitempty"` + Payload []byte `protobuf:"bytes,12,opt,name=payload,proto3" json:"payload,omitempty"` // Types that are assignable to Metadata: + // // *MessageBody_TriggerRegistrationMetadata // *MessageBody_TriggerEventMetadata Metadata isMessageBody_Metadata `protobuf_oneof:"metadata"` @@ -250,6 +260,13 @@ func (x *MessageBody) GetError() Error { return Error_OK } +func (x *MessageBody) GetErrorMsg() string { + if x != nil { + return x.ErrorMsg + } + return "" +} + func (x *MessageBody) GetPayload() []byte { if x != nil { return x.Payload @@ -283,11 +300,11 @@ type isMessageBody_Metadata interface { } type MessageBody_TriggerRegistrationMetadata struct { - TriggerRegistrationMetadata *TriggerRegistrationMetadata `protobuf:"bytes,12,opt,name=trigger_registration_metadata,json=triggerRegistrationMetadata,proto3,oneof"` + TriggerRegistrationMetadata *TriggerRegistrationMetadata `protobuf:"bytes,13,opt,name=trigger_registration_metadata,json=triggerRegistrationMetadata,proto3,oneof"` } type MessageBody_TriggerEventMetadata struct { - TriggerEventMetadata *TriggerEventMetadata `protobuf:"bytes,13,opt,name=trigger_event_metadata,json=triggerEventMetadata,proto3,oneof"` + TriggerEventMetadata *TriggerEventMetadata `protobuf:"bytes,14,opt,name=trigger_event_metadata,json=triggerEventMetadata,proto3,oneof"` } func (*MessageBody_TriggerRegistrationMetadata) isMessageBody_Metadata() {} @@ -406,7 +423,7 @@ var file_core_capabilities_remote_types_message_proto_rawDesc = []byte{ 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, - 0x6f, 0x64, 0x79, 0x22, 0xb1, 0x04, 0x0a, 0x0b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, + 0x6f, 0x64, 0x79, 0x22, 0xcd, 0x04, 0x0a, 0x0b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, @@ -427,39 +444,44 @@ var file_core_capabilities_remote_types_message_proto_rawDesc = []byte{ 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x23, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0d, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x69, 0x0a, 0x1d, 0x74, 0x72, - 0x69, 0x67, 0x67, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0c, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x23, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x54, 0x72, 0x69, 0x67, 0x67, - 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x1b, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, - 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x16, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, - 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x54, - 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x14, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0a, 0x0a, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x52, 0x0a, 0x1b, 0x54, 0x72, 0x69, 0x67, 0x67, - 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x33, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, - 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x63, 0x65, - 0x69, 0x76, 0x65, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x63, 0x0a, 0x14, 0x54, - 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x5f, 0x65, - 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, - 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, - 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x73, - 0x2a, 0x40, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, - 0x00, 0x12, 0x15, 0x0a, 0x11, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, - 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x41, 0x50, 0x41, - 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, - 0x10, 0x02, 0x42, 0x20, 0x5a, 0x1e, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x63, 0x61, 0x70, 0x61, 0x62, - 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x2f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2f, 0x74, - 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x73, 0x67, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x73, 0x67, 0x12, 0x18, 0x0a, 0x07, + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x69, 0x0a, 0x1d, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, + 0x72, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, + 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x48, 0x00, 0x52, 0x1b, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x12, 0x54, 0x0a, 0x16, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x5f, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0e, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x2e, 0x54, 0x72, 0x69, 0x67, 0x67, + 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x48, + 0x00, 0x52, 0x14, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x22, 0x52, 0x0a, 0x1b, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x33, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, + 0x76, 0x65, 0x64, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x13, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x63, 0x0a, 0x14, 0x54, 0x72, 0x69, 0x67, 0x67, + 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, + 0x28, 0x0a, 0x10, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x72, 0x69, 0x67, 0x67, + 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, + 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x73, 0x2a, 0x76, 0x0a, 0x05, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x00, 0x12, 0x15, 0x0a, + 0x11, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, + 0x45, 0x44, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x41, 0x50, 0x41, 0x42, 0x49, 0x4c, 0x49, + 0x54, 0x59, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x02, 0x12, 0x13, + 0x0a, 0x0f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, + 0x54, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x04, + 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x5f, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x05, 0x42, 0x20, 0x5a, 0x1e, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x63, 0x61, 0x70, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x2f, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/core/capabilities/remote/types/message.proto b/core/capabilities/remote/types/message.proto index 072accedbc0..4d0507fd1e0 100644 --- a/core/capabilities/remote/types/message.proto +++ b/core/capabilities/remote/types/message.proto @@ -8,6 +8,9 @@ enum Error { OK = 0; VALIDATION_FAILED = 1; CAPABILITY_NOT_FOUND = 2; + INVALID_REQUEST = 3; + TIMEOUT = 4; + INTERNAL_ERROR = 5; } message Message { @@ -26,13 +29,15 @@ message MessageBody { string caller_don_id = 8; string method = 9; Error error = 10; + string errorMsg = 11; // payload contains a CapabilityRequest or CapabilityResponse - bytes payload = 11; + bytes payload = 12; oneof metadata { - TriggerRegistrationMetadata trigger_registration_metadata = 12; - TriggerEventMetadata trigger_event_metadata = 13; + TriggerRegistrationMetadata trigger_registration_metadata = 13; + TriggerEventMetadata trigger_event_metadata = 14; } + } message TriggerRegistrationMetadata { diff --git a/core/capabilities/remote/types/types.go b/core/capabilities/remote/types/types.go index d8307d09f80..a825c42be56 100644 --- a/core/capabilities/remote/types/types.go +++ b/core/capabilities/remote/types/types.go @@ -9,6 +9,7 @@ const ( MethodRegisterTrigger = "RegisterTrigger" MethodUnRegisterTrigger = "UnregisterTrigger" MethodTriggerEvent = "TriggerEvent" + MethodExecute = "Execute" ) //go:generate mockery --quiet --name Dispatcher --output ./mocks/ --case=underscore diff --git a/core/capabilities/syncer.go b/core/capabilities/syncer.go index 2567676bcd2..1c3bbab8fe8 100644 --- a/core/capabilities/syncer.go +++ b/core/capabilities/syncer.go @@ -3,6 +3,7 @@ package capabilities import ( "context" "encoding/hex" + "fmt" "math/big" "slices" "sync" @@ -25,12 +26,14 @@ import ( ) type registrySyncer struct { - peerWrapper p2ptypes.PeerWrapper - registry core.CapabilitiesRegistry - dispatcher remotetypes.Dispatcher - subServices []services.Service - wg sync.WaitGroup - lggr logger.Logger + peerWrapper p2ptypes.PeerWrapper + registry core.CapabilitiesRegistry + dispatcher remotetypes.Dispatcher + subServices []services.Service + networkSetup HardcodedDonNetworkSetup + + wg sync.WaitGroup + lggr logger.Logger } var _ services.Service = ®istrySyncer{} @@ -52,12 +55,14 @@ var defaultStreamConfig = p2ptypes.StreamConfig{ const maxRetryCount = 60 // RegistrySyncer updates local Registry to match its onchain counterpart -func NewRegistrySyncer(peerWrapper p2ptypes.PeerWrapper, registry core.CapabilitiesRegistry, dispatcher remotetypes.Dispatcher, lggr logger.Logger) *registrySyncer { +func NewRegistrySyncer(peerWrapper p2ptypes.PeerWrapper, registry core.CapabilitiesRegistry, dispatcher remotetypes.Dispatcher, lggr logger.Logger, + networkSetup HardcodedDonNetworkSetup) *registrySyncer { return ®istrySyncer{ - peerWrapper: peerWrapper, - registry: registry, - dispatcher: dispatcher, - lggr: lggr, + peerWrapper: peerWrapper, + registry: registry, + dispatcher: dispatcher, + networkSetup: networkSetup, + lggr: lggr, } } @@ -71,92 +76,48 @@ func (s *registrySyncer) Start(ctx context.Context) error { // that reads the configuration from chain (KS-117). func (s *registrySyncer) launch(ctx context.Context) { defer s.wg.Done() - // NOTE: temporary hard-coded DONs - workflowDONPeers := []string{ - "12D3KooWBCF1XT5Wi8FzfgNCqRL76Swv8TRU3TiD4QiJm8NMNX7N", - "12D3KooWG1AyvwmCpZ93J8pBQUE1SuzrjDXnT4BeouncHR3jWLCG", - "12D3KooWGeUKZBRMbx27FUTgBwZa9Ap9Ym92mywwpuqkEtz8XWyv", - "12D3KooW9zYWQv3STmDeNDidyzxsJSTxoCTLicafgfeEz9nhwhC4", - } - triggerDONPeers := []string{ - "12D3KooWBaiTbbRwwt2fbNifiL7Ew9tn3vds9AJE3Nf3eaVBX36m", - "12D3KooWS7JSY9fzSfWgbCE1S3W2LNY6ZVpRuun74moVBkKj6utE", - "12D3KooWMMTDXcWhpVnwrdAer1jnVARTmnr3RyT3v7Djg8ZuoBh9", - "12D3KooWGzVXsKxXsF4zLgxSDM8Gzx1ywq2pZef4PrHMKuVg4K3P", - "12D3KooWSyjmmzjVtCzwN7bXzZQFmWiJRuVcKBerNjVgL7HdLJBW", - "12D3KooWLGz9gzhrNsvyM6XnXS3JRkZoQdEzuAvysovnSChNK5ZK", - "12D3KooWAvZnvknFAfSiUYjATyhzEJLTeKvAzpcLELHi4ogM3GET", - } - triggerDONSigners := []string{ - "0x9CcE7293a4Cc2621b61193135A95928735e4795F", - "0x3c775F20bCB2108C1A818741Ce332Bb5fe0dB925", - "0x50314239e2CF05555ceeD53E7F47eB2A8Eab0dbB", - "0xd76A4f98898c3b9A72b244476d7337b50D54BCd8", - "0x656A873f6895b8a03Fb112dE927d43FA54B2c92A", - "0x5d1e87d87bF2e0cD4Ea64F381a2dbF45e5f0a553", - "0x91d9b0062265514f012Eb8fABA59372fD9520f56", - } - allPeers := make(map[ragetypes.PeerID]p2ptypes.StreamConfig) - addPeersToDONInfo := func(peers []string, donInfo *capabilities.DON) error { - for _, peerID := range peers { - var p ragetypes.PeerID - err := p.UnmarshalText([]byte(peerID)) - if err != nil { - return err - } - allPeers[p] = defaultStreamConfig - donInfo.Members = append(donInfo.Members, p) - } - return nil - } - workflowDonInfo := capabilities.DON{ID: "workflowDon1", F: 1} - if err := addPeersToDONInfo(workflowDONPeers, &workflowDonInfo); err != nil { - s.lggr.Errorw("failed to add peers to workflow DON info", "error", err) - return - } - triggerCapabilityDonInfo := capabilities.DON{ID: "capabilityDon1", F: 1} // NOTE: misconfiguration - should be 2 - if err := addPeersToDONInfo(triggerDONPeers, &triggerCapabilityDonInfo); err != nil { - s.lggr.Errorw("failed to add peers to trigger DON info", "error", err) - return - } - err := s.peerWrapper.GetPeer().UpdateConnections(allPeers) + capId := "streams-trigger" + triggerInfo, err := capabilities.NewRemoteCapabilityInfo( + capId, + capabilities.CapabilityTypeTrigger, + "Remote Trigger", + "v0.0.1", + &s.networkSetup.TriggerCapabilityDonInfo, + ) if err != nil { - s.lggr.Errorw("failed to update connections", "error", err) + s.lggr.Errorw("failed to create capability info for streams-trigger", "error", err) return } - // NOTE: temporary hard-coded capabilities - capId := "streams-trigger" - triggerInfo := capabilities.CapabilityInfo{ - ID: capId, - CapabilityType: capabilities.CapabilityTypeTrigger, - Description: "Remote Trigger", - Version: "0.0.1", - DON: &triggerCapabilityDonInfo, - } - myId := s.peerWrapper.GetPeer().ID().String() + + myId := s.peerWrapper.GetPeer().ID() config := remotetypes.RemoteTriggerConfig{ RegistrationRefreshMs: 20000, RegistrationExpiryMs: 60000, - MinResponsesToAggregate: uint32(triggerCapabilityDonInfo.F) + 1, + MinResponsesToAggregate: uint32(s.networkSetup.TriggerCapabilityDonInfo.F) + 1, } - if slices.Contains(workflowDONPeers, myId) { + err = s.peerWrapper.GetPeer().UpdateConnections(s.networkSetup.allPeers) + if err != nil { + s.lggr.Errorw("failed to update connections", "error", err) + return + } + if s.networkSetup.IsWorkflowDon(myId) { s.lggr.Info("member of a workflow DON - starting remote subscribers") codec := streams.NewCodec(s.lggr) - aggregator := triggers.NewMercuryRemoteAggregator(codec, hexStringsToBytes(triggerDONSigners), int(triggerCapabilityDonInfo.F+1), s.lggr) - triggerCap := remote.NewTriggerSubscriber(config, triggerInfo, triggerCapabilityDonInfo, workflowDonInfo, s.dispatcher, aggregator, s.lggr) + aggregator := triggers.NewMercuryRemoteAggregator(codec, hexStringsToBytes(s.networkSetup.triggerDonSigners), int(s.networkSetup.TriggerCapabilityDonInfo.F+1), s.lggr) + triggerCap := remote.NewTriggerSubscriber(config, triggerInfo, s.networkSetup.TriggerCapabilityDonInfo, s.networkSetup.WorkflowsDonInfo, s.dispatcher, aggregator, s.lggr) err = s.registry.Add(ctx, triggerCap) if err != nil { s.lggr.Errorw("failed to add remote target capability to registry", "error", err) return } - err = s.dispatcher.SetReceiver(capId, triggerCapabilityDonInfo.ID, triggerCap) + err = s.dispatcher.SetReceiver(capId, s.networkSetup.TriggerCapabilityDonInfo.ID, triggerCap) if err != nil { - s.lggr.Errorw("workflow DON failed to set receiver", "capabilityId", capId, "donId", triggerCapabilityDonInfo.ID, "error", err) + s.lggr.Errorw("workflow DON failed to set receiver", "capabilityId", capId, "donId", s.networkSetup.TriggerCapabilityDonInfo.ID, "error", err) return } s.subServices = append(s.subServices, triggerCap) } - if slices.Contains(triggerDONPeers, myId) { + if s.networkSetup.IsTriggerDon(myId) { s.lggr.Info("member of a capability DON - starting remote publishers") /*{ @@ -190,12 +151,12 @@ func (s *registrySyncer) launch(ctx context.Context) { continue } workflowDONs := map[string]capabilities.DON{ - workflowDonInfo.ID: workflowDonInfo, + s.networkSetup.WorkflowsDonInfo.ID: s.networkSetup.WorkflowsDonInfo, } - triggerCap := remote.NewTriggerPublisher(config, underlying, triggerInfo, triggerCapabilityDonInfo, workflowDONs, s.dispatcher, s.lggr) - err = s.dispatcher.SetReceiver(capId, triggerCapabilityDonInfo.ID, triggerCap) + triggerCap := remote.NewTriggerPublisher(config, underlying, triggerInfo, s.networkSetup.TriggerCapabilityDonInfo, workflowDONs, s.dispatcher, s.lggr) + err = s.dispatcher.SetReceiver(capId, s.networkSetup.TriggerCapabilityDonInfo.ID, triggerCap) if err != nil { - s.lggr.Errorw("capability DON failed to set receiver", "capabilityId", capId, "donId", triggerCapabilityDonInfo.ID, "error", err) + s.lggr.Errorw("capability DON failed to set receiver", "capabilityId", capId, "donId", s.networkSetup.TriggerCapabilityDonInfo.ID, "error", err) return } s.subServices = append(s.subServices, triggerCap) @@ -236,6 +197,78 @@ func (s *registrySyncer) Name() string { return "RegistrySyncer" } +// HardcodedDonNetworkSetup is a temporary setup for testing purposes +type HardcodedDonNetworkSetup struct { + workflowDonPeers []string + triggerDonPeers []string + triggerDonSigners []string + allPeers map[ragetypes.PeerID]p2ptypes.StreamConfig + + WorkflowsDonInfo capabilities.DON + TriggerCapabilityDonInfo capabilities.DON +} + +func NewHardcodedDonNetworkSetup() (HardcodedDonNetworkSetup, error) { + result := HardcodedDonNetworkSetup{} + + result.workflowDonPeers = []string{ + "12D3KooWBCF1XT5Wi8FzfgNCqRL76Swv8TRU3TiD4QiJm8NMNX7N", + "12D3KooWG1AyvwmCpZ93J8pBQUE1SuzrjDXnT4BeouncHR3jWLCG", + "12D3KooWGeUKZBRMbx27FUTgBwZa9Ap9Ym92mywwpuqkEtz8XWyv", + "12D3KooW9zYWQv3STmDeNDidyzxsJSTxoCTLicafgfeEz9nhwhC4", + } + result.triggerDonPeers = []string{ + "12D3KooWBaiTbbRwwt2fbNifiL7Ew9tn3vds9AJE3Nf3eaVBX36m", + "12D3KooWS7JSY9fzSfWgbCE1S3W2LNY6ZVpRuun74moVBkKj6utE", + "12D3KooWMMTDXcWhpVnwrdAer1jnVARTmnr3RyT3v7Djg8ZuoBh9", + "12D3KooWGzVXsKxXsF4zLgxSDM8Gzx1ywq2pZef4PrHMKuVg4K3P", + "12D3KooWSyjmmzjVtCzwN7bXzZQFmWiJRuVcKBerNjVgL7HdLJBW", + "12D3KooWLGz9gzhrNsvyM6XnXS3JRkZoQdEzuAvysovnSChNK5ZK", + "12D3KooWAvZnvknFAfSiUYjATyhzEJLTeKvAzpcLELHi4ogM3GET", + } + result.triggerDonSigners = []string{ + "0x9CcE7293a4Cc2621b61193135A95928735e4795F", + "0x3c775F20bCB2108C1A818741Ce332Bb5fe0dB925", + "0x50314239e2CF05555ceeD53E7F47eB2A8Eab0dbB", + "0xd76A4f98898c3b9A72b244476d7337b50D54BCd8", + "0x656A873f6895b8a03Fb112dE927d43FA54B2c92A", + "0x5d1e87d87bF2e0cD4Ea64F381a2dbF45e5f0a553", + "0x91d9b0062265514f012Eb8fABA59372fD9520f56", + } + + result.allPeers = make(map[ragetypes.PeerID]p2ptypes.StreamConfig) + addPeersToDONInfo := func(peers []string, donInfo *capabilities.DON) error { + for _, peerID := range peers { + var p ragetypes.PeerID + err := p.UnmarshalText([]byte(peerID)) + if err != nil { + return err + } + result.allPeers[p] = defaultStreamConfig + donInfo.Members = append(donInfo.Members, p) + } + return nil + } + result.WorkflowsDonInfo = capabilities.DON{ID: "workflowDon1", F: 1} + if err := addPeersToDONInfo(result.workflowDonPeers, &result.WorkflowsDonInfo); err != nil { + return HardcodedDonNetworkSetup{}, fmt.Errorf("failed to add peers to workflow DON info: %w", err) + } + result.TriggerCapabilityDonInfo = capabilities.DON{ID: "capabilityDon1", F: 1} // NOTE: misconfiguration - should be 2 + if err := addPeersToDONInfo(result.triggerDonPeers, &result.TriggerCapabilityDonInfo); err != nil { + return HardcodedDonNetworkSetup{}, fmt.Errorf("failed to add peers to trigger DON info: %w", err) + } + + return result, nil +} + +func (h HardcodedDonNetworkSetup) IsWorkflowDon(id p2ptypes.PeerID) bool { + return slices.Contains(h.workflowDonPeers, id.String()) +} + +func (h HardcodedDonNetworkSetup) IsTriggerDon(id p2ptypes.PeerID) bool { + return slices.Contains(h.triggerDonPeers, id.String()) +} + type mockMercuryDataProducer struct { trigger *triggers.MercuryTriggerService wg sync.WaitGroup diff --git a/core/capabilities/syncer_test.go b/core/capabilities/syncer_test.go index 757135635d8..a654f303a95 100644 --- a/core/capabilities/syncer_test.go +++ b/core/capabilities/syncer_test.go @@ -32,7 +32,9 @@ func TestSyncer_CleanStartClose(t *testing.T) { dispatcher := remoteMocks.NewDispatcher(t) dispatcher.On("SetReceiver", mock.Anything, mock.Anything, mock.Anything).Return(nil) - syncer := coreCapabilities.NewRegistrySyncer(wrapper, registry, dispatcher, lggr) + networkSetup, err := coreCapabilities.NewHardcodedDonNetworkSetup() + require.NoError(t, err) + syncer := coreCapabilities.NewRegistrySyncer(wrapper, registry, dispatcher, lggr, networkSetup) require.NoError(t, syncer.Start(ctx)) require.NoError(t, syncer.Close()) } diff --git a/core/capabilities/targets/mocks/chain_reader.go b/core/capabilities/targets/mocks/chain_reader.go new file mode 100644 index 00000000000..14748c16bb8 --- /dev/null +++ b/core/capabilities/targets/mocks/chain_reader.go @@ -0,0 +1,189 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + query "github.com/smartcontractkit/chainlink-common/pkg/types/query" + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +// ChainReader is an autogenerated mock type for the ChainReader type +type ChainReader struct { + mock.Mock +} + +// Bind provides a mock function with given fields: ctx, bindings +func (_m *ChainReader) Bind(ctx context.Context, bindings []types.BoundContract) error { + ret := _m.Called(ctx, bindings) + + if len(ret) == 0 { + panic("no return value specified for Bind") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []types.BoundContract) error); ok { + r0 = rf(ctx, bindings) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Close provides a mock function with given fields: +func (_m *ChainReader) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetLatestValue provides a mock function with given fields: ctx, contractName, method, params, returnVal +func (_m *ChainReader) GetLatestValue(ctx context.Context, contractName string, method string, params interface{}, returnVal interface{}) error { + ret := _m.Called(ctx, contractName, method, params, returnVal) + + if len(ret) == 0 { + panic("no return value specified for GetLatestValue") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, interface{}, interface{}) error); ok { + r0 = rf(ctx, contractName, method, params, returnVal) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// HealthReport provides a mock function with given fields: +func (_m *ChainReader) HealthReport() map[string]error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for HealthReport") + } + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// Name provides a mock function with given fields: +func (_m *ChainReader) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// QueryKey provides a mock function with given fields: ctx, contractName, filter, limitAndSort, sequenceDataType +func (_m *ChainReader) QueryKey(ctx context.Context, contractName string, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType interface{}) ([]types.Sequence, error) { + ret := _m.Called(ctx, contractName, filter, limitAndSort, sequenceDataType) + + if len(ret) == 0 { + panic("no return value specified for QueryKey") + } + + var r0 []types.Sequence + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, query.KeyFilter, query.LimitAndSort, interface{}) ([]types.Sequence, error)); ok { + return rf(ctx, contractName, filter, limitAndSort, sequenceDataType) + } + if rf, ok := ret.Get(0).(func(context.Context, string, query.KeyFilter, query.LimitAndSort, interface{}) []types.Sequence); ok { + r0 = rf(ctx, contractName, filter, limitAndSort, sequenceDataType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.Sequence) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, query.KeyFilter, query.LimitAndSort, interface{}) error); ok { + r1 = rf(ctx, contractName, filter, limitAndSort, sequenceDataType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Ready provides a mock function with given fields: +func (_m *ChainReader) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Start provides a mock function with given fields: _a0 +func (_m *ChainReader) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewChainReader creates a new instance of ChainReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewChainReader(t interface { + mock.TestingT + Cleanup(func()) +}) *ChainReader { + mock := &ChainReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/targets/mocks/chain_writer.go b/core/capabilities/targets/mocks/chain_writer.go new file mode 100644 index 00000000000..d8a5a677a11 --- /dev/null +++ b/core/capabilities/targets/mocks/chain_writer.go @@ -0,0 +1,109 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink-common/pkg/types" + + uuid "github.com/google/uuid" +) + +// ChainWriter is an autogenerated mock type for the ChainWriter type +type ChainWriter struct { + mock.Mock +} + +// GetFeeComponents provides a mock function with given fields: ctx +func (_m *ChainWriter) GetFeeComponents(ctx context.Context) (*types.ChainFeeComponents, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetFeeComponents") + } + + var r0 *types.ChainFeeComponents + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*types.ChainFeeComponents, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *types.ChainFeeComponents); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.ChainFeeComponents) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransactionStatus provides a mock function with given fields: ctx, transactionID +func (_m *ChainWriter) GetTransactionStatus(ctx context.Context, transactionID uuid.UUID) (types.TransactionStatus, error) { + ret := _m.Called(ctx, transactionID) + + if len(ret) == 0 { + panic("no return value specified for GetTransactionStatus") + } + + var r0 types.TransactionStatus + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (types.TransactionStatus, error)); ok { + return rf(ctx, transactionID) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) types.TransactionStatus); ok { + r0 = rf(ctx, transactionID) + } else { + r0 = ret.Get(0).(types.TransactionStatus) + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, transactionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubmitTransaction provides a mock function with given fields: ctx, contractName, method, args, transactionID, toAddress, meta, value +func (_m *ChainWriter) SubmitTransaction(ctx context.Context, contractName string, method string, args interface{}, transactionID uuid.UUID, toAddress string, meta *types.TxMeta, value big.Int) error { + ret := _m.Called(ctx, contractName, method, args, transactionID, toAddress, meta, value) + + if len(ret) == 0 { + panic("no return value specified for SubmitTransaction") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, interface{}, uuid.UUID, string, *types.TxMeta, big.Int) error); ok { + r0 = rf(ctx, contractName, method, args, transactionID, toAddress, meta, value) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewChainWriter creates a new instance of ChainWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewChainWriter(t interface { + mock.TestingT + Cleanup(func()) +}) *ChainWriter { + mock := &ChainWriter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/targets/write_target.go b/core/capabilities/targets/write_target.go index 6a55499a5a8..be5c779bf05 100644 --- a/core/capabilities/targets/write_target.go +++ b/core/capabilities/targets/write_target.go @@ -2,98 +2,77 @@ package targets import ( "context" + "encoding/hex" "fmt" + "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/mitchellh/mapstructure" - - chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/values" - txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" "github.com/smartcontractkit/chainlink/v2/core/logger" ) -var forwardABI = evmtypes.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) - -func InitializeWrite(registry core.CapabilitiesRegistry, legacyEVMChains legacyevm.LegacyChainContainer, lggr logger.Logger) error { - for _, chain := range legacyEVMChains.Slice() { - capability := NewEvmWrite(chain, lggr) - if err := registry.Add(context.TODO(), capability); err != nil { - return err - } - } - return nil -} - var ( - _ capabilities.ActionCapability = &EvmWrite{} + _ capabilities.ActionCapability = &WriteTarget{} ) -const ( - defaultGasLimit = 200000 - signedReportField = "signed_report" -) +const signedReportField = "signed_report" -type EvmWrite struct { - chain legacyevm.Chain +type WriteTarget struct { + cr commontypes.ContractReader + cw commontypes.ChainWriter + forwarderAddress string capabilities.CapabilityInfo lggr logger.Logger } -func NewEvmWrite(chain legacyevm.Chain, lggr logger.Logger) *EvmWrite { - // generate ID based on chain selector - name := fmt.Sprintf("write_%v", chain.ID()) - chainName, err := chainselectors.NameFromChainId(chain.ID().Uint64()) - if err == nil { - name = fmt.Sprintf("write_%v", chainName) - } - +func NewWriteTarget(lggr logger.Logger, name string, cr commontypes.ContractReader, cw commontypes.ChainWriter, forwarderAddress string) *WriteTarget { info := capabilities.MustNewCapabilityInfo( name, capabilities.CapabilityTypeTarget, "Write target.", "v1.0.0", - nil, ) - return &EvmWrite{ - chain, + logger := lggr.Named("WriteTarget") + + return &WriteTarget{ + cr, + cw, + forwarderAddress, info, - lggr.Named("EvmWrite"), + logger, } } type EvmConfig struct { - ChainID uint Address string } -// TODO: enforce required key presence - -func parseConfig(rawConfig *values.Map) (EvmConfig, error) { - var config EvmConfig - configAny, err := rawConfig.Unwrap() - if err != nil { +func parseConfig(rawConfig *values.Map) (config EvmConfig, err error) { + if err := rawConfig.UnwrapTo(&config); err != nil { return config, err } - err = mapstructure.Decode(configAny, &config) - return config, err + if !common.IsHexAddress(config.Address) { + return config, fmt.Errorf("'%v' is not a valid address", config.Address) + } + return config, nil } -func (cap *EvmWrite) Execute(ctx context.Context, request capabilities.CapabilityRequest) (<-chan capabilities.CapabilityResponse, error) { - cap.lggr.Debugw("Execute", "request", request) - // TODO: idempotency - - txm := cap.chain.TxManager() +func success() <-chan capabilities.CapabilityResponse { + callback := make(chan capabilities.CapabilityResponse) + go func() { + callback <- capabilities.CapabilityResponse{} + close(callback) + }() + return callback +} - config := cap.chain.Config().EVM().ChainWriter() +func (cap *WriteTarget) Execute(ctx context.Context, request capabilities.CapabilityRequest) (<-chan capabilities.CapabilityResponse, error) { + cap.lggr.Debugw("Execute", "request", request) reqConfig, err := parseConfig(request.Config) if err != nil { @@ -107,6 +86,7 @@ func (cap *EvmWrite) Execute(ctx context.Context, request capabilities.Capabilit var inputs struct { Report []byte + Context []byte Signatures [][]byte } if err = signedReport.UnwrapTo(&inputs); err != nil { @@ -116,68 +96,73 @@ func (cap *EvmWrite) Execute(ctx context.Context, request capabilities.Capabilit if inputs.Report == nil { // We received any empty report -- this means we should skip transmission. cap.lggr.Debugw("Skipping empty report", "request", request) - callback := make(chan capabilities.CapabilityResponse) - go func() { - // TODO: cast tx.Error to Err (or Value to Value?) - callback <- capabilities.CapabilityResponse{ - Value: nil, - Err: nil, - } - close(callback) - }() - return callback, nil - } - cap.lggr.Debugw("WriteTarget non-empty report - attempting to push to txmgr", "request", request, "report", inputs.Report, "signatures", inputs.Signatures) + return success(), nil + } + cap.lggr.Debugw("WriteTarget non-empty report - attempting to push to txmgr", "request", request, "reportLen", len(inputs.Report), "reportContextLen", len(inputs.Context), "nSignatures", len(inputs.Signatures)) // TODO: validate encoded report is prefixed with workflowID and executionID that match the request meta - // construct forwarder payload - calldata, err := forwardABI.Pack("report", common.HexToAddress(reqConfig.Address), inputs.Report, inputs.Signatures) + rawExecutionID, err := hex.DecodeString(request.Metadata.WorkflowExecutionID) if err != nil { return nil, err } - - txMeta := &txmgr.TxMeta{ - // FwdrDestAddress could also be set for better logging but it's used for various purposes around Operator Forwarders - WorkflowExecutionID: &request.Metadata.WorkflowExecutionID, + // Check whether value was already transmitted on chain + queryInputs := struct { + Receiver string + WorkflowExecutionID []byte + }{ + Receiver: reqConfig.Address, + WorkflowExecutionID: rawExecutionID, } - strategy := txmgrcommon.NewSendEveryStrategy() - - checker := txmgr.TransmitCheckerSpec{ - CheckerType: txmgr.TransmitCheckerTypeSimulate, + var transmitter common.Address + if err = cap.cr.GetLatestValue(ctx, "forwarder", "getTransmitter", queryInputs, &transmitter); err != nil { + return nil, err } - req := txmgr.TxRequest{ - FromAddress: config.FromAddress().Address(), - ToAddress: config.ForwarderAddress().Address(), - EncodedPayload: calldata, - FeeLimit: uint64(defaultGasLimit), - Meta: txMeta, - Strategy: strategy, - Checker: checker, - // SignalCallback: true, TODO: add code that checks if a workflow id is present, if so, route callback to chainwriter rather than pipeline + if transmitter != common.HexToAddress("0x0") { + // report already transmitted, early return + return success(), nil } - tx, err := txm.CreateTransaction(ctx, req) + + txID, err := uuid.NewUUID() // TODO(archseer): it seems odd that CW expects us to generate an ID, rather than return one if err != nil { return nil, err } - cap.lggr.Debugw("Transaction submitted", "request", request, "transaction", tx) - callback := make(chan capabilities.CapabilityResponse) - go func() { - // TODO: cast tx.Error to Err (or Value to Value?) - callback <- capabilities.CapabilityResponse{ - Value: nil, - Err: nil, - } - close(callback) - }() - return callback, nil + // Note: The codec that ChainWriter uses to encode the parameters for the contract ABI cannot handle + // `nil` values, including for slices. Until the bug is fixed we need to ensure that there are no + // `nil` values passed in the request. + req := struct { + ReceiverAddress string + RawReport []byte + ReportContext []byte + Signatures [][]byte + }{reqConfig.Address, inputs.Report, inputs.Context, inputs.Signatures} + + if req.RawReport == nil { + req.RawReport = make([]byte, 0) + } + + if req.ReportContext == nil { + req.ReportContext = make([]byte, 0) + } + + if req.Signatures == nil { + req.Signatures = make([][]byte, 0) + } + + meta := commontypes.TxMeta{WorkflowExecutionID: &request.Metadata.WorkflowExecutionID} + value := big.NewInt(0) + if err := cap.cw.SubmitTransaction(ctx, "forwarder", "report", req, txID, cap.forwarderAddress, &meta, *value); err != nil { + return nil, err + } + cap.lggr.Debugw("Transaction submitted", "request", request, "transaction", txID) + return success(), nil } -func (cap *EvmWrite) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { +func (cap *WriteTarget) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { return nil } -func (cap *EvmWrite) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { +func (cap *WriteTarget) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { return nil } diff --git a/core/capabilities/targets/write_target_test.go b/core/capabilities/targets/write_target_test.go index f4cab88a739..6964a9617ea 100644 --- a/core/capabilities/targets/write_target_test.go +++ b/core/capabilities/targets/write_target_test.go @@ -1,57 +1,45 @@ package targets_test import ( - "math/big" + "context" + "errors" "testing" + "github.com/ethereum/go-ethereum/common" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" - txmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr/mocks" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm/mocks" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets/mocks" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest" "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" ) -var forwardABI = types.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) +//go:generate mockery --quiet --name ChainWriter --srcpkg=github.com/smartcontractkit/chainlink-common/pkg/types --output ./mocks/ --case=underscore +//go:generate mockery --quiet --name ChainReader --srcpkg=github.com/smartcontractkit/chainlink-common/pkg/types --output ./mocks/ --case=underscore -func TestEvmWrite(t *testing.T) { - chain := evmmocks.NewChain(t) +func TestWriteTarget(t *testing.T) { + lggr := logger.TestLogger(t) + ctx := context.Background() - txManager := txmmocks.NewMockEvmTxManager(t) - chain.On("ID").Return(big.NewInt(11155111)) - chain.On("TxManager").Return(txManager) - - cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - a := testutils.NewAddress() - addr, err := types.NewEIP55Address(a.Hex()) - require.NoError(t, err) - c.EVM[0].ChainWriter.FromAddress = &addr + cw := mocks.NewChainWriter(t) + cr := mocks.NewChainReader(t) - forwarderA := testutils.NewAddress() - forwarderAddr, err := types.NewEIP55Address(forwarderA.Hex()) - require.NoError(t, err) - c.EVM[0].ChainWriter.ForwarderAddress = &forwarderAddr - }) - evmcfg := evmtest.NewChainScopedConfig(t, cfg) - chain.On("Config").Return(evmcfg) + forwarderA := testutils.NewAddress() + forwarderAddr := forwarderA.Hex() - capability := targets.NewEvmWrite(chain, logger.TestLogger(t)) - ctx := testutils.Context(t) + writeTarget := targets.NewWriteTarget(lggr, "Test", cr, cw, forwarderAddr) + require.NotNil(t, writeTarget) - config, err := values.NewMap(map[string]any{}) + config, err := values.NewMap(map[string]any{ + "Address": forwarderAddr, + }) require.NoError(t, err) - inputs, err := values.NewMap(map[string]any{ + validInputs, err := values.NewMap(map[string]any{ "signed_report": map[string]any{ "report": []byte{1, 2, 3}, "signatures": [][]byte{}, @@ -59,76 +47,93 @@ func TestEvmWrite(t *testing.T) { }) require.NoError(t, err) - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "hello", - }, - Config: config, - Inputs: inputs, - } - - txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, nil).Run(func(args mock.Arguments) { - req := args.Get(1).(txmgr.TxRequest) - payload := make(map[string]any) - method := forwardABI.Methods["report"] - err = method.Inputs.UnpackIntoMap(payload, req.EncodedPayload[4:]) - require.NoError(t, err) - require.Equal(t, []byte{0x1, 0x2, 0x3}, payload["rawReport"]) - require.Equal(t, [][]byte{}, payload["signatures"]) + cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmitter", mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + transmitter := args.Get(4).(*common.Address) + *transmitter = common.HexToAddress("0x0") + }).Once() + + cw.On("SubmitTransaction", mock.Anything, "forwarder", "report", mock.Anything, mock.Anything, forwarderAddr, mock.Anything, mock.Anything).Return(nil).Once() + + t.Run("succeeds with valid report", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: validInputs, + } + + ch, err2 := writeTarget.Execute(ctx, req) + require.NoError(t, err2) + response := <-ch + require.NotNil(t, response) }) - ch, err := capability.Execute(ctx, req) - require.NoError(t, err) - - response := <-ch - require.Nil(t, response.Err) -} - -func TestEvmWrite_EmptyReport(t *testing.T) { - chain := evmmocks.NewChain(t) - - txManager := txmmocks.NewMockEvmTxManager(t) - chain.On("ID").Return(big.NewInt(11155111)) - chain.On("TxManager").Return(txManager) - - cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - a := testutils.NewAddress() - addr, err := types.NewEIP55Address(a.Hex()) - require.NoError(t, err) - c.EVM[0].ChainWriter.FromAddress = &addr - - forwarderA := testutils.NewAddress() - forwarderAddr, err := types.NewEIP55Address(forwarderA.Hex()) - require.NoError(t, err) - c.EVM[0].ChainWriter.ForwarderAddress = &forwarderAddr + t.Run("succeeds with empty report", func(t *testing.T) { + emptyInputs, err2 := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": nil, + }, + "signatures": [][]byte{}, + }) + + require.NoError(t, err2) + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowExecutionID: "test-id", + }, + Config: config, + Inputs: emptyInputs, + } + + ch, err2 := writeTarget.Execute(ctx, req) + require.NoError(t, err2) + response := <-ch + require.Nil(t, response.Value) }) - evmcfg := evmtest.NewChainScopedConfig(t, cfg) - chain.On("Config").Return(evmcfg) - - capability := targets.NewEvmWrite(chain, logger.TestLogger(t)) - ctx := testutils.Context(t) - - config, err := values.NewMap(map[string]any{}) - require.NoError(t, err) - inputs, err := values.NewMap(map[string]any{ - "signed_report": map[string]any{ - "report": nil, - }, + t.Run("fails when ChainReader's GetLatestValue returns error", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: validInputs, + } + cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmitter", mock.Anything, mock.Anything).Return(errors.New("reader error")) + + _, err = writeTarget.Execute(ctx, req) + require.Error(t, err) }) - require.NoError(t, err) - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "hello", - }, - Config: config, - Inputs: inputs, - } + t.Run("fails when ChainWriter's SubmitTransaction returns error", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: validInputs, + } + cw.On("SubmitTransaction", mock.Anything, "forwarder", "report", mock.Anything, mock.Anything, forwarderAddr, mock.Anything, mock.Anything).Return(errors.New("writer error")) + + _, err = writeTarget.Execute(ctx, req) + require.Error(t, err) + }) - ch, err := capability.Execute(ctx, req) - require.NoError(t, err) + t.Run("fails with invalid config", func(t *testing.T) { + invalidConfig, err := values.NewMap(map[string]any{ + "Address": "invalid-address", + }) + require.NoError(t, err) - response := <-ch - require.Nil(t, response.Err) + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: invalidConfig, + Inputs: validInputs, + } + _, err = writeTarget.Execute(ctx, req) + require.Error(t, err) + }) } diff --git a/core/capabilities/transmission/local_target_capability.go b/core/capabilities/transmission/local_target_capability.go new file mode 100644 index 00000000000..4fddd93d403 --- /dev/null +++ b/core/capabilities/transmission/local_target_capability.go @@ -0,0 +1,59 @@ +package transmission + +import ( + "context" + "fmt" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink/v2/core/logger" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" +) + +// LocalTargetCapability handles the transmission protocol required for a target capability that exists in the same don as +// the caller. +type LocalTargetCapability struct { + lggr logger.Logger + capabilities.TargetCapability + peerID p2ptypes.PeerID + don capabilities.DON +} + +func NewLocalTargetCapability(lggr logger.Logger, peerID p2ptypes.PeerID, don capabilities.DON, underlying capabilities.TargetCapability) *LocalTargetCapability { + return &LocalTargetCapability{ + TargetCapability: underlying, + lggr: lggr, + peerID: peerID, + don: don, + } +} + +func (l *LocalTargetCapability) Execute(ctx context.Context, req capabilities.CapabilityRequest) (<-chan capabilities.CapabilityResponse, error) { + if req.Config == nil || req.Config.Underlying["schedule"] == nil { + l.lggr.Debug("no schedule found, executing immediately") + return l.TargetCapability.Execute(ctx, req) + } + + tc, err := ExtractTransmissionConfig(req.Config) + if err != nil { + return nil, fmt.Errorf("failed to extract transmission config from request config: %w", err) + } + + peerIDToTransmissionDelay, err := GetPeerIDToTransmissionDelay(l.don.Members, l.don.Config.SharedSecret, + req.Metadata.WorkflowID+req.Metadata.WorkflowExecutionID, tc) + if err != nil { + return nil, fmt.Errorf("failed to get peer ID to transmission delay map: %w", err) + } + + delay, existsForPeerID := peerIDToTransmissionDelay[l.peerID] + if !existsForPeerID { + return nil, nil + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(delay): + return l.TargetCapability.Execute(ctx, req) + } +} diff --git a/core/services/workflows/execution_strategy_test.go b/core/capabilities/transmission/local_target_capability_test.go similarity index 73% rename from core/services/workflows/execution_strategy_test.go rename to core/capabilities/transmission/local_target_capability_test.go index bdf782c87b9..19d51b492ff 100644 --- a/core/services/workflows/execution_strategy_test.go +++ b/core/capabilities/transmission/local_target_capability_test.go @@ -1,6 +1,7 @@ -package workflows +package transmission import ( + "context" "crypto/rand" "encoding/hex" "testing" @@ -25,6 +26,8 @@ func TestScheduledExecutionStrategy_LocalDON(t *testing.T) { var gotTime time.Time var called bool + log := logger.TestLogger(t) + // Our capability has DONInfo == nil, so we'll treat it as a local // capability and use the local DON Info to determine the transmission // schedule. @@ -34,7 +37,6 @@ func TestScheduledExecutionStrategy_LocalDON(t *testing.T) { capabilities.CapabilityTypeTarget, "a write capability targeting polygon mumbai testnet", "v1.0.0", - nil, ), func(req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { gotTime = time.Now() @@ -43,8 +45,6 @@ func TestScheduledExecutionStrategy_LocalDON(t *testing.T) { }, ) - l := logger.TestLogger(t) - // The combination of this key and the metadata above // will yield the permutation [3, 2, 0, 1] key, err := hex.DecodeString("fb13ca015a9ec60089c7141e9522de79") @@ -139,19 +139,17 @@ func TestScheduledExecutionStrategy_LocalDON(t *testing.T) { randKey(), randKey(), } - don := &capabilities.DON{ + don := capabilities.DON{ Members: ids, Config: capabilities.DONConfig{ SharedSecret: [16]byte(key), }, } peerID := ids[tc.position] - de := scheduledExecution{ - DON: don, - PeerID: &peerID, - Position: tc.position, - } - _, err = de.Apply(tests.Context(t), l, mt, req) + localTargetCapability := NewLocalTargetCapability(log, peerID, don, mt) + + _, err = localTargetCapability.Execute(tests.Context(t), req) + require.NoError(t, err) require.True(t, called) @@ -168,3 +166,40 @@ func randKey() [32]byte { } return [32]byte(key) } + +type mockCapability struct { + capabilities.CapabilityInfo + capabilities.CallbackExecutable + response chan capabilities.CapabilityResponse + transform func(capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) +} + +func newMockCapability(info capabilities.CapabilityInfo, transform func(capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error)) *mockCapability { + return &mockCapability{ + transform: transform, + CapabilityInfo: info, + response: make(chan capabilities.CapabilityResponse, 10), + } +} + +func (m *mockCapability) Execute(ctx context.Context, req capabilities.CapabilityRequest) (<-chan capabilities.CapabilityResponse, error) { + cr, err := m.transform(req) + if err != nil { + return nil, err + } + + ch := make(chan capabilities.CapabilityResponse, 10) + + m.response <- cr + ch <- cr + close(ch) + return ch, nil +} + +func (m *mockCapability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { + return nil +} + +func (m *mockCapability) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { + return nil +} diff --git a/core/capabilities/transmission/transmission.go b/core/capabilities/transmission/transmission.go new file mode 100644 index 00000000000..5121a9bf9f3 --- /dev/null +++ b/core/capabilities/transmission/transmission.go @@ -0,0 +1,107 @@ +package transmission + +import ( + "fmt" + "time" + + "golang.org/x/crypto/sha3" + + "github.com/smartcontractkit/libocr/permutation" + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + + "github.com/smartcontractkit/chainlink-common/pkg/values" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" +) + +var ( + // S = [N] + Schedule_AllAtOnce = "allAtOnce" + // S = [1 * N] + Schedule_OneAtATime = "oneAtATime" +) + +type TransmissionConfig struct { + Schedule string + DeltaStage time.Duration +} + +func ExtractTransmissionConfig(config *values.Map) (TransmissionConfig, error) { + var tc struct { + DeltaStage string + Schedule string + } + err := config.UnwrapTo(&tc) + if err != nil { + return TransmissionConfig{}, fmt.Errorf("failed to unwrap tranmission config from value map: %w", err) + } + + duration, err := time.ParseDuration(tc.DeltaStage) + if err != nil { + return TransmissionConfig{}, fmt.Errorf("failed to parse DeltaStage %s as duration: %w", tc.DeltaStage, err) + } + + return TransmissionConfig{ + Schedule: tc.Schedule, + DeltaStage: duration, + }, nil +} + +// GetPeerIDToTransmissionDelay returns a map of PeerID to the time.Duration that the node with that PeerID should wait +// before transmitting. If a node is not in the map, it should not transmit. The sharedSecret is shared by nodes in the +// same DON and used to generate a deterministic schedule for the transmission delays. +func GetPeerIDToTransmissionDelay(donPeerIDs []ragep2ptypes.PeerID, sharedSecret [16]byte, transmissionID string, tc TransmissionConfig) (map[p2ptypes.PeerID]time.Duration, error) { + donMemberCount := len(donPeerIDs) + key := transmissionScheduleSeed(sharedSecret, transmissionID) + schedule, err := createTransmissionSchedule(tc.Schedule, donMemberCount) + if err != nil { + return nil, err + } + + picked := permutation.Permutation(donMemberCount, key) + + peerIDToTransmissionDelay := map[p2ptypes.PeerID]time.Duration{} + for i, peerID := range donPeerIDs { + delay := delayFor(i, schedule, picked, tc.DeltaStage) + if delay != nil { + peerIDToTransmissionDelay[peerID] = *delay + } + } + return peerIDToTransmissionDelay, nil +} + +func delayFor(position int, schedule []int, permutation []int, deltaStage time.Duration) *time.Duration { + sum := 0 + for i, s := range schedule { + sum += s + if permutation[position] < sum { + result := time.Duration(i) * deltaStage + return &result + } + } + + return nil +} + +func createTransmissionSchedule(scheduleType string, N int) ([]int, error) { + switch scheduleType { + case Schedule_AllAtOnce: + return []int{N}, nil + case Schedule_OneAtATime: + sch := []int{} + for i := 0; i < N; i++ { + sch = append(sch, 1) + } + return sch, nil + } + return nil, fmt.Errorf("unknown schedule type %s", scheduleType) +} + +func transmissionScheduleSeed(sharedSecret [16]byte, transmissionID string) [16]byte { + hash := sha3.NewLegacyKeccak256() + hash.Write(sharedSecret[:]) + hash.Write([]byte(transmissionID)) + + var key [16]byte + copy(key[:], hash.Sum(nil)) + return key +} diff --git a/core/capabilities/transmission/transmission_test.go b/core/capabilities/transmission/transmission_test.go new file mode 100644 index 00000000000..bbdaaa27fe2 --- /dev/null +++ b/core/capabilities/transmission/transmission_test.go @@ -0,0 +1,101 @@ +package transmission + +import ( + "encoding/hex" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/values" + p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" +) + +func Test_GetPeerIDToTransmissionDelay(t *testing.T) { + peer1 := [32]byte([]byte(fmt.Sprintf("%-32s", "one"))) + peer2 := [32]byte([]byte(fmt.Sprintf("%-32s", "two"))) + peer3 := [32]byte([]byte(fmt.Sprintf("%-32s", "three"))) + peer4 := [32]byte([]byte(fmt.Sprintf("%-32s", "four"))) + + ids := []p2ptypes.PeerID{ + peer1, peer2, peer3, peer4, + } + + testCases := []struct { + name string + peerName string + sharedSecret string + schedule string + deltaStage string + workflowExecutionID string + expectedDelays map[string]time.Duration + }{ + { + "TestOneAtATime", + "one", + "fb13ca015a9ec60089c7141e9522de79", + "oneAtATime", + "100ms", + "mock-execution-id", + map[string]time.Duration{ + "one": 300 * time.Millisecond, + "two": 200 * time.Millisecond, + "three": 0 * time.Millisecond, + "four": 100 * time.Millisecond, + }, + }, + { + "TestAllAtOnce", + "one", + "fb13ca015a9ec60089c7141e9522de79", + "allAtOnce", + "100ms", + "mock-execution-id", + map[string]time.Duration{ + "one": 0 * time.Millisecond, + "two": 0 * time.Millisecond, + "three": 0 * time.Millisecond, + "four": 0 * time.Millisecond, + }, + }, + { + "TestOneAtATimeWithDifferentExecutionID", + "one", + "fb13ca015a9ec60089c7141e9522de79", + "oneAtATime", + "100ms", + "mock-execution-id2", + map[string]time.Duration{ + "one": 0 * time.Millisecond, + "two": 300 * time.Millisecond, + "three": 100 * time.Millisecond, + "four": 200 * time.Millisecond, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sharedSecret, err := hex.DecodeString(tc.sharedSecret) + require.NoError(t, err) + + m, err := values.NewMap(map[string]any{ + "schedule": tc.schedule, + "deltaStage": tc.deltaStage, + }) + require.NoError(t, err) + transmissionCfg, err := ExtractTransmissionConfig(m) + require.NoError(t, err) + + peerIdToDelay, err := GetPeerIDToTransmissionDelay(ids, [16]byte(sharedSecret), "mock-workflow-id"+tc.workflowExecutionID, transmissionCfg) + require.NoError(t, err) + + assert.Equal(t, tc.expectedDelays["one"], peerIdToDelay[peer1]) + assert.Equal(t, tc.expectedDelays["two"], peerIdToDelay[peer2]) + assert.Equal(t, tc.expectedDelays["three"], peerIdToDelay[peer3]) + assert.Equal(t, tc.expectedDelays["four"], peerIdToDelay[peer4]) + }) + } +} diff --git a/core/chains/evm/client/chain_client.go b/core/chains/evm/client/chain_client.go index 04b1ff29387..a28d8ab4a9f 100644 --- a/core/chains/evm/client/chain_client.go +++ b/core/chains/evm/client/chain_client.go @@ -19,9 +19,87 @@ import ( evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) +const queryTimeout = 10 * time.Second +const BALANCE_OF_ADDRESS_FUNCTION_SELECTOR = "0x70a08231" + var _ Client = (*chainClient)(nil) -// TODO-1663: rename this to client, once the client.go file is deprecated. +//go:generate mockery --quiet --name Client --output ./mocks/ --case=underscore + +// Client is the interface used to interact with an ethereum node. +type Client interface { + Dial(ctx context.Context) error + Close() + // ChainID locally stored for quick access + ConfiguredChainID() *big.Int + // ChainID RPC call + ChainID() (*big.Int, error) + + // NodeStates returns a map of node Name->node state + // It might be nil or empty, e.g. for mock clients etc + NodeStates() map[string]string + + TokenBalance(ctx context.Context, address common.Address, contractAddress common.Address) (*big.Int, error) + BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) + LINKBalance(ctx context.Context, address common.Address, linkAddress common.Address) (*commonassets.Link, error) + + // Wrapped RPC methods + CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error + BatchCallContext(ctx context.Context, b []rpc.BatchElem) error + // BatchCallContextAll calls BatchCallContext for every single node including + // sendonlys. + // CAUTION: This should only be used for mass re-transmitting transactions, it + // might have unexpected effects to use it for anything else. + BatchCallContextAll(ctx context.Context, b []rpc.BatchElem) error + + // HeadByNumber and HeadByHash is a reimplemented version due to a + // difference in how block header hashes are calculated by Parity nodes + // running on Kovan, Avalanche and potentially others. We have to return our own wrapper type to capture the + // correct hash from the RPC response. + HeadByNumber(ctx context.Context, n *big.Int) (*evmtypes.Head, error) + HeadByHash(ctx context.Context, n common.Hash) (*evmtypes.Head, error) + SubscribeNewHead(ctx context.Context, ch chan<- *evmtypes.Head) (ethereum.Subscription, error) + LatestFinalizedBlock(ctx context.Context) (head *evmtypes.Head, err error) + + SendTransactionReturnCode(ctx context.Context, tx *types.Transaction, fromAddress common.Address) (commonclient.SendTxReturnCode, error) + + // Wrapped Geth client methods + // blockNumber can be specified as `nil` to imply latest block + // if blocks, transactions, or receipts are not found - a nil result and an error are returned + // these methods may not be compatible with non Ethereum chains as return types may follow different formats + // suggested options: use HeadByNumber/HeadByHash (above) or CallContext and parse with custom types + SendTransaction(ctx context.Context, tx *types.Transaction) error + CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) + PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) + PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) + SequenceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (evmtypes.Nonce, error) + TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) + TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) + BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) + BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) + FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) + SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) + EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) + SuggestGasPrice(ctx context.Context) (*big.Int, error) + SuggestGasTipCap(ctx context.Context) (*big.Int, error) + LatestBlockHeight(ctx context.Context) (*big.Int, error) + + HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) + HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) + + CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) + PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) + + IsL2() bool + + // Simulate the transaction prior to sending to catch zk out-of-counters errors ahead of time + CheckTxValidity(ctx context.Context, from common.Address, to common.Address, data []byte) *SendError +} + +func ContextWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) { + return context.WithTimeout(context.Background(), queryTimeout) +} + type chainClient struct { multiNode commonclient.MultiNode[ *big.Int, diff --git a/core/chains/evm/client/chain_client_test.go b/core/chains/evm/client/chain_client_test.go index 268a552fda4..f18ec539677 100644 --- a/core/chains/evm/client/chain_client_test.go +++ b/core/chains/evm/client/chain_client_test.go @@ -1,25 +1,750 @@ package client_test import ( + "context" + "encoding/json" "errors" + "fmt" "math/big" + "net/http/httptest" + "net/url" + "os" + "strings" + "sync/atomic" "testing" "time" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" + pkgerrors "github.com/pkg/errors" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" commonclient "github.com/smartcontractkit/chainlink/v2/common/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" ) +func mustNewChainClient(t *testing.T, wsURL string, sendonlys ...url.URL) client.Client { + return mustNewChainClientWithChainID(t, wsURL, testutils.FixtureChainID, sendonlys...) +} + +func mustNewChainClientWithChainID(t *testing.T, wsURL string, chainID *big.Int, sendonlys ...url.URL) client.Client { + cfg := client.TestNodePoolConfig{ + NodeSelectionMode: commonclient.NodeSelectionModeRoundRobin, + } + c, err := client.NewChainClientWithTestNode(t, cfg, time.Second*0, cfg.NodeLeaseDuration, wsURL, nil, sendonlys, 42, chainID) + require.NoError(t, err) + return c +} + +func TestEthClient_TransactionReceipt(t *testing.T) { + t.Parallel() + + txHash := "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" + + mustReadResult := func(t *testing.T, file string) []byte { + response, err := os.ReadFile(file) + require.NoError(t, err) + var resp struct { + Result json.RawMessage `json:"result"` + } + err = json.Unmarshal(response, &resp) + require.NoError(t, err) + return resp.Result + } + + t.Run("happy path", func(t *testing.T) { + result := mustReadResult(t, "../../../testdata/jsonrpc/getTransactionReceipt.json") + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if assert.Equal(t, "eth_getTransactionReceipt", method) && assert.True(t, params.IsArray()) && + assert.Equal(t, txHash, params.Array()[0].String()) { + resp.Result = string(result) + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + hash := common.HexToHash(txHash) + receipt, err := ethClient.TransactionReceipt(tests.Context(t), hash) + require.NoError(t, err) + assert.Equal(t, hash, receipt.TxHash) + assert.Equal(t, big.NewInt(11), receipt.BlockNumber) + }) + + t.Run("no tx hash, returns ethereum.NotFound", func(t *testing.T) { + result := mustReadResult(t, "../../../testdata/jsonrpc/getTransactionReceipt_notFound.json") + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if assert.Equal(t, "eth_getTransactionReceipt", method) && assert.True(t, params.IsArray()) && + assert.Equal(t, txHash, params.Array()[0].String()) { + resp.Result = string(result) + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + hash := common.HexToHash(txHash) + _, err = ethClient.TransactionReceipt(tests.Context(t), hash) + require.Equal(t, ethereum.NotFound, pkgerrors.Cause(err)) + }) +} + +func TestEthClient_PendingNonceAt(t *testing.T) { + t.Parallel() + + address := testutils.NewAddress() + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if !assert.Equal(t, "eth_getTransactionCount", method) || !assert.True(t, params.IsArray()) { + return + } + arr := params.Array() + if assert.Equal(t, strings.ToLower(address.Hex()), strings.ToLower(arr[0].String())) && + assert.Equal(t, "pending", arr[1].String()) { + resp.Result = `"0x100"` + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + result, err := ethClient.PendingNonceAt(tests.Context(t), address) + require.NoError(t, err) + + var expected uint64 = 256 + require.Equal(t, result, expected) +} + +func TestEthClient_BalanceAt(t *testing.T) { + t.Parallel() + + largeBalance, _ := big.NewInt(0).SetString("100000000000000000000", 10) + address := testutils.NewAddress() + + cases := []struct { + name string + balance *big.Int + }{ + {"basic", big.NewInt(256)}, + {"larger than signed 64 bit integer", largeBalance}, + } + + for _, test := range cases { + test := test + t.Run(test.name, func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if assert.Equal(t, "eth_getBalance", method) && assert.True(t, params.IsArray()) && + assert.Equal(t, strings.ToLower(address.Hex()), strings.ToLower(params.Array()[0].String())) { + resp.Result = `"` + hexutil.EncodeBig(test.balance) + `"` + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + result, err := ethClient.BalanceAt(tests.Context(t), address, nil) + require.NoError(t, err) + assert.Equal(t, test.balance, result) + }) + } +} + +func TestEthClient_LatestBlockHeight(t *testing.T) { + t.Parallel() + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if !assert.Equal(t, "eth_blockNumber", method) { + return + } + resp.Result = `"0x100"` + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + result, err := ethClient.LatestBlockHeight(tests.Context(t)) + require.NoError(t, err) + require.Equal(t, big.NewInt(256), result) +} + +func TestEthClient_GetERC20Balance(t *testing.T) { + t.Parallel() + ctx := tests.Context(t) + + expectedBig, _ := big.NewInt(0).SetString("100000000000000000000000000000000000000", 10) + + cases := []struct { + name string + balance *big.Int + }{ + {"small", big.NewInt(256)}, + {"big", expectedBig}, + } + + for _, test := range cases { + test := test + t.Run(test.name, func(t *testing.T) { + contractAddress := testutils.NewAddress() + userAddress := testutils.NewAddress() + functionSelector := evmtypes.HexToFunctionSelector(client.BALANCE_OF_ADDRESS_FUNCTION_SELECTOR) // balanceOf(address) + txData := utils.ConcatBytes(functionSelector.Bytes(), common.LeftPadBytes(userAddress.Bytes(), utils.EVMWordByteLen)) + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if !assert.Equal(t, "eth_call", method) || !assert.True(t, params.IsArray()) { + return + } + arr := params.Array() + callArgs := arr[0] + if assert.True(t, callArgs.IsObject()) && + assert.Equal(t, strings.ToLower(contractAddress.Hex()), callArgs.Get("to").String()) && + assert.Equal(t, hexutil.Encode(txData), callArgs.Get("data").String()) && + assert.Equal(t, "latest", arr[1].String()) { + resp.Result = `"` + hexutil.EncodeBig(test.balance) + `"` + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + result, err := ethClient.TokenBalance(ctx, userAddress, contractAddress) + require.NoError(t, err) + assert.Equal(t, test.balance, result) + }) + } +} + +func TestReceipt_UnmarshalEmptyBlockHash(t *testing.T) { + t.Parallel() + + input := `{ + "transactionHash": "0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374", + "gasUsed": "0x1", + "cumulativeGasUsed": "0x1", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": "0x8bf99b", + "blockHash": null + }` + + var receipt types.Receipt + err := json.Unmarshal([]byte(input), &receipt) + require.NoError(t, err) +} + +func TestEthClient_HeaderByNumber(t *testing.T) { + t.Parallel() + + expectedBlockNum := big.NewInt(1) + expectedBlockHash := "0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a" + + cases := []struct { + name string + expectedRequestBlock *big.Int + expectedResponseBlock int64 + error error + rpcResp string + }{ + {"happy geth", expectedBlockNum, expectedBlockNum.Int64(), nil, + `{"difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`}, + {"happy parity", expectedBlockNum, expectedBlockNum.Int64(), nil, + `{"author":"0xd1aeb42885a43b72b518182ef893125814811048","difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sealFields":["0xa00f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","0x880ece08ea8c49dfd9"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`}, + {"missing header", expectedBlockNum, 0, fmt.Errorf("no live nodes available for chain %s", testutils.FixtureChainID.String()), + `null`}, + } + + for _, test := range cases { + test := test + t.Run(test.name, func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if !assert.Equal(t, "eth_getBlockByNumber", method) || !assert.True(t, params.IsArray()) { + return + } + arr := params.Array() + blockNumStr := arr[0].String() + var blockNum hexutil.Big + err := blockNum.UnmarshalText([]byte(blockNumStr)) + if assert.NoError(t, err) && assert.Equal(t, test.expectedRequestBlock, blockNum.ToInt()) && + assert.Equal(t, false, arr[1].Bool()) { + resp.Result = test.rpcResp + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(tests.Context(t), 5*time.Second) + result, err := ethClient.HeadByNumber(ctx, expectedBlockNum) + if test.error != nil { + require.Error(t, err, test.error) + } else { + require.NoError(t, err) + require.Equal(t, expectedBlockHash, result.Hash.Hex()) + require.Equal(t, test.expectedResponseBlock, result.Number) + require.Zero(t, testutils.FixtureChainID.Cmp(result.EVMChainID.ToInt())) + } + cancel() + }) + } +} + +func TestEthClient_SendTransaction_NoSecondaryURL(t *testing.T) { + t.Parallel() + + tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + } + if !assert.Equal(t, "eth_sendRawTransaction", method) { + return + } + resp.Result = `"` + tx.Hash().Hex() + `"` + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + err = ethClient.SendTransaction(tests.Context(t), tx) + assert.NoError(t, err) +} + +func TestEthClient_SendTransaction_WithSecondaryURLs(t *testing.T) { + t.Parallel() + + tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) + + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + } + return + }).WSURL().String() + + rpcSrv := rpc.NewServer() + t.Cleanup(rpcSrv.Stop) + service := sendTxService{chainID: testutils.FixtureChainID} + err := rpcSrv.RegisterName("eth", &service) + require.NoError(t, err) + ts := httptest.NewServer(rpcSrv) + t.Cleanup(ts.Close) + + sendonlyURL, err := url.Parse(ts.URL) + require.NoError(t, err) + + ethClient := mustNewChainClient(t, wsURL, *sendonlyURL, *sendonlyURL) + err = ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + err = ethClient.SendTransaction(tests.Context(t), tx) + require.NoError(t, err) + + // Unfortunately it's a bit tricky to test this, since there is no + // synchronization. We have to rely on timing instead. + require.Eventually(t, func() bool { return service.sentCount.Load() == int32(2) }, tests.WaitTimeout(t), 500*time.Millisecond) +} + +func TestEthClient_SendTransactionReturnCode(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) + + t.Run("returns Fatal error type when error message is fatal", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "invalid sender" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.Fatal) + }) + + t.Run("returns TransactionAlreadyKnown error type when error message is nonce too low", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "nonce too low" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.TransactionAlreadyKnown) + }) + + t.Run("returns Successful error type when there is no error message", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.NoError(t, err) + assert.Equal(t, errType, commonclient.Successful) + }) + + t.Run("returns Underpriced error type when transaction is terminally underpriced", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "transaction underpriced" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.Underpriced) + }) + + t.Run("returns Unsupported error type when error message is queue full", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "queue full" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.Unsupported) + }) + + t.Run("returns Retryable error type when there is a transaction gap", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "NonceGap" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.Retryable) + }) + + t.Run("returns InsufficientFunds error type when the sender address doesn't have enough funds", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "insufficient funds for transfer" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.InsufficientFunds) + }) + + t.Run("returns ExceedsFeeCap error type when gas price is too high for the node", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "Transaction fee cap exceeded" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.ExceedsMaxFee) + }) + + t.Run("returns Unknown error type when the error can't be categorized", func(t *testing.T) { + wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + switch method { + case "eth_subscribe": + resp.Result = `"0x00"` + resp.Notify = headResult + return + case "eth_unsubscribe": + resp.Result = "true" + return + case "eth_sendRawTransaction": + resp.Result = `"` + tx.Hash().Hex() + `"` + resp.Error.Message = "some random error" + } + return + }).WSURL().String() + + ethClient := mustNewChainClient(t, wsURL) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) + assert.Error(t, err) + assert.Equal(t, errType, commonclient.Unknown) + }) +} + +type sendTxService struct { + chainID *big.Int + sentCount atomic.Int32 +} + +func (x *sendTxService) ChainId(ctx context.Context) (*hexutil.Big, error) { + return (*hexutil.Big)(x.chainID), nil +} + +func (x *sendTxService) SendRawTransaction(ctx context.Context, signRawTx hexutil.Bytes) error { + x.sentCount.Add(1) + return nil +} + +func TestEthClient_SubscribeNewHead(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(tests.Context(t), tests.WaitTimeout(t)) + defer cancel() + + chainId := big.NewInt(123456) + wsURL := testutils.NewWSServer(t, chainId, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { + if method == "eth_unsubscribe" { + resp.Result = "true" + return + } + assert.Equal(t, "eth_subscribe", method) + if assert.True(t, params.IsArray()) && assert.Equal(t, "newHeads", params.Array()[0].String()) { + resp.Result = `"0x00"` + resp.Notify = headResult + } + return + }).WSURL().String() + + ethClient := mustNewChainClientWithChainID(t, wsURL, chainId) + err := ethClient.Dial(tests.Context(t)) + require.NoError(t, err) + + headCh := make(chan *evmtypes.Head) + sub, err := ethClient.SubscribeNewHead(ctx, headCh) + require.NoError(t, err) + + select { + case err := <-sub.Err(): + t.Fatal(err) + case <-ctx.Done(): + t.Fatal(ctx.Err()) + case h := <-headCh: + require.NotNil(t, h.EVMChainID) + require.Zero(t, chainId.Cmp(h.EVMChainID.ToInt())) + } + sub.Unsubscribe() +} + func newMockRpc(t *testing.T) *mocks.RPCClient { mockRpc := mocks.NewRPCClient(t) mockRpc.On("Dial", mock.Anything).Return(nil).Once() @@ -71,3 +796,110 @@ func TestChainClient_BatchCallContext(t *testing.T) { } }) } + +func TestEthClient_ErroringClient(t *testing.T) { + t.Parallel() + ctx := tests.Context(t) + + // Empty node means there are no active nodes to select from, causing client to always return error. + erroringClient := client.NewChainClientWithEmptyNode(t, commonclient.NodeSelectionModeRoundRobin, time.Second*0, time.Second*0, testutils.FixtureChainID) + + _, err := erroringClient.BalanceAt(ctx, common.Address{}, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + err = erroringClient.BatchCallContext(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + err = erroringClient.BatchCallContextAll(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.BlockByHash(ctx, common.Hash{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.BlockByNumber(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + err = erroringClient.CallContext(ctx, nil, "") + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.CallContract(ctx, ethereum.CallMsg{}, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + // TODO-1663: test actual ChainID() call once client.go is deprecated. + id, err := erroringClient.ChainID() + require.Equal(t, id, testutils.FixtureChainID) + //require.Equal(t, err, commonclient.ErroringNodeError) + require.Equal(t, err, nil) + + _, err = erroringClient.CodeAt(ctx, common.Address{}, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + id = erroringClient.ConfiguredChainID() + require.Equal(t, id, testutils.FixtureChainID) + + err = erroringClient.Dial(ctx) + require.ErrorContains(t, err, "no available nodes for chain") + + _, err = erroringClient.EstimateGas(ctx, ethereum.CallMsg{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.FilterLogs(ctx, ethereum.FilterQuery{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.HeaderByHash(ctx, common.Hash{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.HeaderByNumber(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.HeadByHash(ctx, common.Hash{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.HeadByNumber(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.LINKBalance(ctx, common.Address{}, common.Address{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.LatestBlockHeight(ctx) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.PendingCodeAt(ctx, common.Address{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.PendingNonceAt(ctx, common.Address{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + err = erroringClient.SendTransaction(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + code, err := erroringClient.SendTransactionReturnCode(ctx, nil, common.Address{}) + require.Equal(t, code, commonclient.Unknown) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.SequenceAt(ctx, common.Address{}, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.SubscribeNewHead(ctx, nil) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.SuggestGasPrice(ctx) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.SuggestGasTipCap(ctx) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.TokenBalance(ctx, common.Address{}, common.Address{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.TransactionByHash(ctx, common.Hash{}) + require.Equal(t, err, commonclient.ErroringNodeError) + + _, err = erroringClient.TransactionReceipt(ctx, common.Hash{}) + require.Equal(t, err, commonclient.ErroringNodeError) +} + +const headResult = client.HeadResult diff --git a/core/chains/evm/client/client.go b/core/chains/evm/client/client.go deleted file mode 100644 index 9628c74b9ab..00000000000 --- a/core/chains/evm/client/client.go +++ /dev/null @@ -1,380 +0,0 @@ -package client - -import ( - "context" - "fmt" - "math/big" - "strings" - "time" - - "github.com/smartcontractkit/chainlink-common/pkg/assets" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - - commonclient "github.com/smartcontractkit/chainlink/v2/common/client" - "github.com/smartcontractkit/chainlink/v2/common/config" - htrktypes "github.com/smartcontractkit/chainlink/v2/common/headtracker/types" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" - ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - pkgerrors "github.com/pkg/errors" -) - -const queryTimeout = 10 * time.Second -const BALANCE_OF_ADDRESS_FUNCTION_SELECTOR = "0x70a08231" - -//go:generate mockery --quiet --name Client --output ./mocks/ --case=underscore - -// Client is the interface used to interact with an ethereum node. -type Client interface { - Dial(ctx context.Context) error - Close() - // ChainID locally stored for quick access - ConfiguredChainID() *big.Int - // ChainID RPC call - ChainID() (*big.Int, error) - - // NodeStates returns a map of node Name->node state - // It might be nil or empty, e.g. for mock clients etc - NodeStates() map[string]string - - TokenBalance(ctx context.Context, address common.Address, contractAddress common.Address) (*big.Int, error) - BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) - LINKBalance(ctx context.Context, address common.Address, linkAddress common.Address) (*assets.Link, error) - - // Wrapped RPC methods - CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error - BatchCallContext(ctx context.Context, b []rpc.BatchElem) error - // BatchCallContextAll calls BatchCallContext for every single node including - // sendonlys. - // CAUTION: This should only be used for mass re-transmitting transactions, it - // might have unexpected effects to use it for anything else. - BatchCallContextAll(ctx context.Context, b []rpc.BatchElem) error - - // HeadByNumber and HeadByHash is a reimplemented version due to a - // difference in how block header hashes are calculated by Parity nodes - // running on Kovan, Avalanche and potentially others. We have to return our own wrapper type to capture the - // correct hash from the RPC response. - HeadByNumber(ctx context.Context, n *big.Int) (*evmtypes.Head, error) - HeadByHash(ctx context.Context, n common.Hash) (*evmtypes.Head, error) - SubscribeNewHead(ctx context.Context, ch chan<- *evmtypes.Head) (ethereum.Subscription, error) - LatestFinalizedBlock(ctx context.Context) (head *evmtypes.Head, err error) - - SendTransactionReturnCode(ctx context.Context, tx *types.Transaction, fromAddress common.Address) (commonclient.SendTxReturnCode, error) - - // Wrapped Geth client methods - // blockNumber can be specified as `nil` to imply latest block - // if blocks, transactions, or receipts are not found - a nil result and an error are returned - // these methods may not be compatible with non Ethereum chains as return types may follow different formats - // suggested options: use HeadByNumber/HeadByHash (above) or CallContext and parse with custom types - SendTransaction(ctx context.Context, tx *types.Transaction) error - CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) - PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) - PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) - SequenceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (evmtypes.Nonce, error) - TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) - TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) - BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) - BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) - FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) - SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) - EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) - SuggestGasPrice(ctx context.Context) (*big.Int, error) - SuggestGasTipCap(ctx context.Context) (*big.Int, error) - LatestBlockHeight(ctx context.Context) (*big.Int, error) - - HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) - HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) - - CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) - PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) - - IsL2() bool - - // Simulate the transaction prior to sending to catch zk out-of-counters errors ahead of time - CheckTxValidity(ctx context.Context, from common.Address, to common.Address, data []byte) *SendError -} - -func ContextWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) { - return context.WithTimeout(context.Background(), queryTimeout) -} - -// client represents an abstract client that manages connections to -// multiple nodes for a single chain id -type client struct { - logger logger.SugaredLogger - pool *Pool -} - -var _ Client = (*client)(nil) -var _ htrktypes.Client[*evmtypes.Head, ethereum.Subscription, *big.Int, common.Hash] = (*client)(nil) - -// NewClientWithNodes instantiates a client from a list of nodes -// Currently only supports one primary -// -// Deprecated: use [NewChainClient] -func NewClientWithNodes(lggr logger.Logger, selectionMode string, leaseDuration time.Duration, noNewHeadsThreshold time.Duration, primaryNodes []Node, sendOnlyNodes []SendOnlyNode, chainID *big.Int, chainType config.ChainType) (*client, error) { - pool := NewPool(lggr, selectionMode, leaseDuration, noNewHeadsThreshold, primaryNodes, sendOnlyNodes, chainID, chainType) - return &client{ - logger: logger.Sugared(lggr), - pool: pool, - }, nil -} - -// Dial opens websocket connections if necessary and sanity-checks that the -// node's remote chain ID matches the local one -func (client *client) Dial(ctx context.Context) error { - if err := client.pool.Dial(ctx); err != nil { - return pkgerrors.Wrap(err, "failed to dial pool") - } - return nil -} - -func (client *client) Close() { - client.pool.Close() -} - -func (client *client) NodeStates() (states map[string]string) { - states = make(map[string]string) - for _, n := range client.pool.nodes { - states[n.Name()] = n.State().String() - } - for _, s := range client.pool.sendonlys { - states[s.Name()] = s.State().String() - } - return -} - -// CallArgs represents the data used to call the balance method of a contract. -// "To" is the address of the ERC contract. "Data" is the message sent -// to the contract. "From" is the sender address. -type CallArgs struct { - From common.Address `json:"from"` - To common.Address `json:"to"` - Data hexutil.Bytes `json:"data"` -} - -// TokenBalance returns the balance of the given address for the token contract address. -func (client *client) TokenBalance(ctx context.Context, address common.Address, contractAddress common.Address) (*big.Int, error) { - result := "" - numLinkBigInt := new(big.Int) - functionSelector := evmtypes.HexToFunctionSelector(BALANCE_OF_ADDRESS_FUNCTION_SELECTOR) // balanceOf(address) - data := utils.ConcatBytes(functionSelector.Bytes(), common.LeftPadBytes(address.Bytes(), utils.EVMWordByteLen)) - args := CallArgs{ - To: contractAddress, - Data: data, - } - err := client.CallContext(ctx, &result, "eth_call", args, "latest") - if err != nil { - return numLinkBigInt, err - } - if _, ok := numLinkBigInt.SetString(result, 0); !ok { - return nil, fmt.Errorf("failed to parse int: %s", result) - } - return numLinkBigInt, nil -} - -// LINKBalance returns the balance of LINK at the given address -func (client *client) LINKBalance(ctx context.Context, address common.Address, linkAddress common.Address) (*assets.Link, error) { - balance, err := client.TokenBalance(ctx, address, linkAddress) - if err != nil { - return assets.NewLinkFromJuels(0), err - } - return (*assets.Link)(balance), nil -} - -func (client *client) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { - return client.pool.BalanceAt(ctx, account, blockNumber) -} - -// We wrap the GethClient's `TransactionReceipt` method so that we can ignore the error that arises -// when we're talking to a Parity node that has no receipt yet. -func (client *client) TransactionReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) { - receipt, err = client.pool.TransactionReceipt(ctx, txHash) - - if err != nil && strings.Contains(err.Error(), "missing required field") { - return nil, ethereum.NotFound - } - return -} - -func (client *client) TransactionByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, err error) { - return client.pool.TransactionByHash(ctx, txHash) -} - -func (client *client) ConfiguredChainID() *big.Int { - return client.pool.chainID -} - -func (client *client) ChainID() (*big.Int, error) { - return client.pool.ChainID(), nil -} - -func (client *client) HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) { - return client.pool.HeaderByNumber(ctx, n) -} - -func (client *client) HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) { - return client.pool.HeaderByHash(ctx, h) -} - -func (client *client) SendTransactionReturnCode(ctx context.Context, tx *types.Transaction, fromAddress common.Address) (commonclient.SendTxReturnCode, error) { - err := client.SendTransaction(ctx, tx) - returnCode := ClassifySendError(err, nil, client.logger, tx, fromAddress, client.pool.ChainType().IsL2()) - return returnCode, err -} - -// SendTransaction also uses the sendonly HTTP RPC URLs if set -func (client *client) SendTransaction(ctx context.Context, tx *types.Transaction) error { - return client.pool.SendTransaction(ctx, tx) -} - -func (client *client) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { - return client.pool.PendingNonceAt(ctx, account) -} - -func (client *client) SequenceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (evmtypes.Nonce, error) { - nonce, err := client.pool.NonceAt(ctx, account, blockNumber) - return evmtypes.Nonce(nonce), err -} - -func (client *client) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { - return client.pool.PendingCodeAt(ctx, account) -} - -func (client *client) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { - return client.pool.EstimateGas(ctx, call) -} - -// SuggestGasPrice calls the RPC node to get a suggested gas price. -// WARNING: It is not recommended to ever use this result for anything -// important. There are a number of issues with asking the RPC node to provide a -// gas estimate; it is not reliable. Unless you really have a good reason to -// use this, you should probably use core node's internal gas estimator -// instead. -func (client *client) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - return client.pool.SuggestGasPrice(ctx) -} - -func (client *client) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - return client.pool.CallContract(ctx, msg, blockNumber) -} - -func (client *client) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { - return client.pool.PendingCallContract(ctx, msg) -} - -func (client *client) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { - return client.pool.CodeAt(ctx, account, blockNumber) -} - -func (client *client) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - return client.pool.BlockByNumber(ctx, number) -} - -func (client *client) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - return client.pool.BlockByHash(ctx, hash) -} - -func (client *client) LatestBlockHeight(ctx context.Context) (*big.Int, error) { - var height big.Int - h, err := client.pool.BlockNumber(ctx) - return height.SetUint64(h), err -} - -func (client *client) HeadByNumber(ctx context.Context, number *big.Int) (head *evmtypes.Head, err error) { - hex := ToBlockNumArg(number) - err = client.pool.CallContext(ctx, &head, "eth_getBlockByNumber", hex, false) - if err != nil { - return nil, err - } - if head == nil { - err = ethereum.NotFound - return - } - head.EVMChainID = ubig.New(client.ConfiguredChainID()) - return -} - -func (client *client) HeadByHash(ctx context.Context, hash common.Hash) (head *evmtypes.Head, err error) { - err = client.pool.CallContext(ctx, &head, "eth_getBlockByHash", hash.Hex(), false) - if err != nil { - return nil, err - } - if head == nil { - err = ethereum.NotFound - return - } - head.EVMChainID = ubig.New(client.ConfiguredChainID()) - return -} - -func ToBlockNumArg(number *big.Int) string { - if number == nil { - return "latest" - } - return hexutil.EncodeBig(number) -} - -func (client *client) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { - return client.pool.FilterLogs(ctx, q) -} - -func (client *client) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - client.logger.Debugw("evmclient.Client#SubscribeFilterLogs(...)", - "q", q, - ) - return client.pool.SubscribeFilterLogs(ctx, q, ch) -} - -func (client *client) SubscribeNewHead(ctx context.Context, ch chan<- *evmtypes.Head) (ethereum.Subscription, error) { - csf := newChainIDSubForwarder(client.ConfiguredChainID(), ch) - err := csf.start(client.pool.EthSubscribe(ctx, csf.srcCh, "newHeads")) - if err != nil { - return nil, err - } - return csf, nil -} - -func (client *client) EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) { - return client.pool.EthSubscribe(ctx, channel, args...) -} - -func (client *client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - return client.pool.CallContext(ctx, result, method, args...) -} - -func (client *client) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - return client.pool.BatchCallContext(ctx, b) -} - -func (client *client) BatchCallContextAll(ctx context.Context, b []rpc.BatchElem) error { - return client.pool.BatchCallContextAll(ctx, b) -} - -// SuggestGasTipCap calls the RPC node to get a suggested gas tip cap. -// WARNING: It is not recommended to ever use this result for anything -// important. There are a number of issues with asking the RPC node to provide a -// gas estimate; it is not reliable. Unless you really have a good reason to -// use this, you should probably use core node's internal gas estimator -// instead. -func (client *client) SuggestGasTipCap(ctx context.Context) (tipCap *big.Int, err error) { - return client.pool.SuggestGasTipCap(ctx) -} - -func (client *client) IsL2() bool { - return client.pool.ChainType().IsL2() -} - -func (client *client) LatestFinalizedBlock(_ context.Context) (*evmtypes.Head, error) { - return nil, pkgerrors.New("not implemented. client was deprecated. New methods are added only to satisfy type constraints while we are migrating to new alternatives") -} - -func (client *client) CheckTxValidity(ctx context.Context, from common.Address, to common.Address, data []byte) *SendError { - return NewSendError(pkgerrors.New("not implemented. client was deprecated. New methods are added only to satisfy type constraints while we are migrating to new alternatives")) -} diff --git a/core/chains/evm/client/client_test.go b/core/chains/evm/client/client_test.go deleted file mode 100644 index 0aa457ceaca..00000000000 --- a/core/chains/evm/client/client_test.go +++ /dev/null @@ -1,915 +0,0 @@ -package client_test - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "net/http/httptest" - "net/url" - "os" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - pkgerrors "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" - - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - - commonclient "github.com/smartcontractkit/chainlink/v2/common/client" - - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" -) - -func mustNewClient(t *testing.T, wsURL string, sendonlys ...url.URL) client.Client { - return mustNewClientWithChainID(t, wsURL, testutils.FixtureChainID, sendonlys...) -} - -func mustNewClientWithChainID(t *testing.T, wsURL string, chainID *big.Int, sendonlys ...url.URL) client.Client { - cfg := client.TestNodePoolConfig{ - NodeSelectionMode: client.NodeSelectionMode_RoundRobin, - } - c, err := client.NewClientWithTestNode(t, cfg, time.Second*0, wsURL, nil, sendonlys, 42, chainID) - require.NoError(t, err) - return c -} - -func mustNewChainClient(t *testing.T, wsURL string, sendonlys ...url.URL) client.Client { - return mustNewChainClientWithChainID(t, wsURL, testutils.FixtureChainID, sendonlys...) -} - -func mustNewChainClientWithChainID(t *testing.T, wsURL string, chainID *big.Int, sendonlys ...url.URL) client.Client { - cfg := client.TestNodePoolConfig{ - NodeSelectionMode: client.NodeSelectionMode_RoundRobin, - } - c, err := client.NewChainClientWithTestNode(t, cfg, time.Second*0, cfg.NodeLeaseDuration, wsURL, nil, sendonlys, 42, chainID) - require.NoError(t, err) - return c -} - -func mustNewClients(t *testing.T, wsURL string, sendonlys ...url.URL) []client.Client { - var clients []client.Client - clients = append(clients, mustNewClient(t, wsURL, sendonlys...)) - clients = append(clients, mustNewChainClient(t, wsURL, sendonlys...)) - return clients -} - -func mustNewClientsWithChainID(t *testing.T, wsURL string, chainID *big.Int, sendonlys ...url.URL) []client.Client { - var clients []client.Client - clients = append(clients, mustNewClientWithChainID(t, wsURL, chainID, sendonlys...)) - clients = append(clients, mustNewChainClientWithChainID(t, wsURL, chainID, sendonlys...)) - return clients -} - -func TestEthClient_TransactionReceipt(t *testing.T) { - t.Parallel() - - txHash := "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" - - mustReadResult := func(t *testing.T, file string) []byte { - response, err := os.ReadFile(file) - require.NoError(t, err) - var resp struct { - Result json.RawMessage `json:"result"` - } - err = json.Unmarshal(response, &resp) - require.NoError(t, err) - return resp.Result - } - - t.Run("happy path", func(t *testing.T) { - result := mustReadResult(t, "../../../testdata/jsonrpc/getTransactionReceipt.json") - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if assert.Equal(t, "eth_getTransactionReceipt", method) && assert.True(t, params.IsArray()) && - assert.Equal(t, txHash, params.Array()[0].String()) { - resp.Result = string(result) - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - hash := common.HexToHash(txHash) - receipt, err := ethClient.TransactionReceipt(tests.Context(t), hash) - require.NoError(t, err) - assert.Equal(t, hash, receipt.TxHash) - assert.Equal(t, big.NewInt(11), receipt.BlockNumber) - } - }) - - t.Run("no tx hash, returns ethereum.NotFound", func(t *testing.T) { - result := mustReadResult(t, "../../../testdata/jsonrpc/getTransactionReceipt_notFound.json") - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if assert.Equal(t, "eth_getTransactionReceipt", method) && assert.True(t, params.IsArray()) && - assert.Equal(t, txHash, params.Array()[0].String()) { - resp.Result = string(result) - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - hash := common.HexToHash(txHash) - _, err = ethClient.TransactionReceipt(tests.Context(t), hash) - require.Equal(t, ethereum.NotFound, pkgerrors.Cause(err)) - } - }) -} - -func TestEthClient_PendingNonceAt(t *testing.T) { - t.Parallel() - - address := testutils.NewAddress() - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if !assert.Equal(t, "eth_getTransactionCount", method) || !assert.True(t, params.IsArray()) { - return - } - arr := params.Array() - if assert.Equal(t, strings.ToLower(address.Hex()), strings.ToLower(arr[0].String())) && - assert.Equal(t, "pending", arr[1].String()) { - resp.Result = `"0x100"` - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - result, err := ethClient.PendingNonceAt(tests.Context(t), address) - require.NoError(t, err) - - var expected uint64 = 256 - require.Equal(t, result, expected) - } -} - -func TestEthClient_BalanceAt(t *testing.T) { - t.Parallel() - - largeBalance, _ := big.NewInt(0).SetString("100000000000000000000", 10) - address := testutils.NewAddress() - - cases := []struct { - name string - balance *big.Int - }{ - {"basic", big.NewInt(256)}, - {"larger than signed 64 bit integer", largeBalance}, - } - - for _, test := range cases { - test := test - t.Run(test.name, func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if assert.Equal(t, "eth_getBalance", method) && assert.True(t, params.IsArray()) && - assert.Equal(t, strings.ToLower(address.Hex()), strings.ToLower(params.Array()[0].String())) { - resp.Result = `"` + hexutil.EncodeBig(test.balance) + `"` - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - result, err := ethClient.BalanceAt(tests.Context(t), address, nil) - require.NoError(t, err) - assert.Equal(t, test.balance, result) - } - }) - } -} - -func TestEthClient_LatestBlockHeight(t *testing.T) { - t.Parallel() - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if !assert.Equal(t, "eth_blockNumber", method) { - return - } - resp.Result = `"0x100"` - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - result, err := ethClient.LatestBlockHeight(tests.Context(t)) - require.NoError(t, err) - require.Equal(t, big.NewInt(256), result) - } -} - -func TestEthClient_GetERC20Balance(t *testing.T) { - t.Parallel() - ctx := tests.Context(t) - - expectedBig, _ := big.NewInt(0).SetString("100000000000000000000000000000000000000", 10) - - cases := []struct { - name string - balance *big.Int - }{ - {"small", big.NewInt(256)}, - {"big", expectedBig}, - } - - for _, test := range cases { - test := test - t.Run(test.name, func(t *testing.T) { - contractAddress := testutils.NewAddress() - userAddress := testutils.NewAddress() - functionSelector := evmtypes.HexToFunctionSelector(client.BALANCE_OF_ADDRESS_FUNCTION_SELECTOR) // balanceOf(address) - txData := utils.ConcatBytes(functionSelector.Bytes(), common.LeftPadBytes(userAddress.Bytes(), utils.EVMWordByteLen)) - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if !assert.Equal(t, "eth_call", method) || !assert.True(t, params.IsArray()) { - return - } - arr := params.Array() - callArgs := arr[0] - if assert.True(t, callArgs.IsObject()) && - assert.Equal(t, strings.ToLower(contractAddress.Hex()), callArgs.Get("to").String()) && - assert.Equal(t, hexutil.Encode(txData), callArgs.Get("data").String()) && - assert.Equal(t, "latest", arr[1].String()) { - resp.Result = `"` + hexutil.EncodeBig(test.balance) + `"` - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - result, err := ethClient.TokenBalance(ctx, userAddress, contractAddress) - require.NoError(t, err) - assert.Equal(t, test.balance, result) - } - }) - } -} - -func TestReceipt_UnmarshalEmptyBlockHash(t *testing.T) { - t.Parallel() - - input := `{ - "transactionHash": "0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374", - "gasUsed": "0x1", - "cumulativeGasUsed": "0x1", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "blockNumber": "0x8bf99b", - "blockHash": null - }` - - var receipt types.Receipt - err := json.Unmarshal([]byte(input), &receipt) - require.NoError(t, err) -} - -func TestEthClient_HeaderByNumber(t *testing.T) { - t.Parallel() - - expectedBlockNum := big.NewInt(1) - expectedBlockHash := "0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a" - - cases := []struct { - name string - expectedRequestBlock *big.Int - expectedResponseBlock int64 - error error - rpcResp string - }{ - {"happy geth", expectedBlockNum, expectedBlockNum.Int64(), nil, - `{"difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`}, - {"happy parity", expectedBlockNum, expectedBlockNum.Int64(), nil, - `{"author":"0xd1aeb42885a43b72b518182ef893125814811048","difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sealFields":["0xa00f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","0x880ece08ea8c49dfd9"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`}, - {"missing header", expectedBlockNum, 0, fmt.Errorf("no live nodes available for chain %s", testutils.FixtureChainID.String()), - `null`}, - } - - for _, test := range cases { - test := test - t.Run(test.name, func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if !assert.Equal(t, "eth_getBlockByNumber", method) || !assert.True(t, params.IsArray()) { - return - } - arr := params.Array() - blockNumStr := arr[0].String() - var blockNum hexutil.Big - err := blockNum.UnmarshalText([]byte(blockNumStr)) - if assert.NoError(t, err) && assert.Equal(t, test.expectedRequestBlock, blockNum.ToInt()) && - assert.Equal(t, false, arr[1].Bool()) { - resp.Result = test.rpcResp - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - ctx, cancel := context.WithTimeout(tests.Context(t), 5*time.Second) - result, err := ethClient.HeadByNumber(ctx, expectedBlockNum) - if test.error != nil { - require.Error(t, err, test.error) - } else { - require.NoError(t, err) - require.Equal(t, expectedBlockHash, result.Hash.Hex()) - require.Equal(t, test.expectedResponseBlock, result.Number) - require.Zero(t, testutils.FixtureChainID.Cmp(result.EVMChainID.ToInt())) - } - cancel() - } - }) - } -} - -func TestEthClient_SendTransaction_NoSecondaryURL(t *testing.T) { - t.Parallel() - - tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - if !assert.Equal(t, "eth_sendRawTransaction", method) { - return - } - resp.Result = `"` + tx.Hash().Hex() + `"` - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - err = ethClient.SendTransaction(tests.Context(t), tx) - assert.NoError(t, err) - } -} - -func TestEthClient_SendTransaction_WithSecondaryURLs(t *testing.T) { - t.Parallel() - - tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) - - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - } - return - }).WSURL().String() - - rpcSrv := rpc.NewServer() - t.Cleanup(rpcSrv.Stop) - service := sendTxService{chainID: testutils.FixtureChainID} - err := rpcSrv.RegisterName("eth", &service) - require.NoError(t, err) - ts := httptest.NewServer(rpcSrv) - t.Cleanup(ts.Close) - - sendonlyURL, err := url.Parse(ts.URL) - require.NoError(t, err) - - clients := mustNewClients(t, wsURL, *sendonlyURL, *sendonlyURL) - for _, ethClient := range clients { - err = ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - err = ethClient.SendTransaction(tests.Context(t), tx) - require.NoError(t, err) - } - - // Unfortunately it's a bit tricky to test this, since there is no - // synchronization. We have to rely on timing instead. - require.Eventually(t, func() bool { return service.sentCount.Load() == int32(len(clients)*2) }, tests.WaitTimeout(t), 500*time.Millisecond) -} - -func TestEthClient_SendTransactionReturnCode(t *testing.T) { - t.Parallel() - - fromAddress := testutils.NewAddress() - tx := testutils.NewLegacyTransaction(uint64(42), testutils.NewAddress(), big.NewInt(142), 242, big.NewInt(342), []byte{1, 2, 3}) - - t.Run("returns Fatal error type when error message is fatal", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "invalid sender" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.Fatal) - } - }) - - t.Run("returns TransactionAlreadyKnown error type when error message is nonce too low", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "nonce too low" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.TransactionAlreadyKnown) - } - }) - - t.Run("returns Successful error type when there is no error message", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.NoError(t, err) - assert.Equal(t, errType, commonclient.Successful) - } - }) - - t.Run("returns Underpriced error type when transaction is terminally underpriced", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "transaction underpriced" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.Underpriced) - } - }) - - t.Run("returns Unsupported error type when error message is queue full", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "queue full" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.Unsupported) - } - }) - - t.Run("returns Retryable error type when there is a transaction gap", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "NonceGap" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.Retryable) - } - }) - - t.Run("returns InsufficientFunds error type when the sender address doesn't have enough funds", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "insufficient funds for transfer" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.InsufficientFunds) - } - }) - - t.Run("returns ExceedsFeeCap error type when gas price is too high for the node", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "Transaction fee cap exceeded" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.ExceedsMaxFee) - } - }) - - t.Run("returns Unknown error type when the error can't be categorized", func(t *testing.T) { - wsURL := testutils.NewWSServer(t, testutils.FixtureChainID, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "eth_sendRawTransaction": - resp.Result = `"` + tx.Hash().Hex() + `"` - resp.Error.Message = "some random error" - } - return - }).WSURL().String() - - clients := mustNewClients(t, wsURL) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - errType, err := ethClient.SendTransactionReturnCode(tests.Context(t), tx, fromAddress) - assert.Error(t, err) - assert.Equal(t, errType, commonclient.Unknown) - } - }) -} - -type sendTxService struct { - chainID *big.Int - sentCount atomic.Int32 -} - -func (x *sendTxService) ChainId(ctx context.Context) (*hexutil.Big, error) { - return (*hexutil.Big)(x.chainID), nil -} - -func (x *sendTxService) SendRawTransaction(ctx context.Context, signRawTx hexutil.Bytes) error { - x.sentCount.Add(1) - return nil -} - -func TestEthClient_SubscribeNewHead(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(tests.Context(t), tests.WaitTimeout(t)) - defer cancel() - - chainId := big.NewInt(123456) - wsURL := testutils.NewWSServer(t, chainId, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - if method == "eth_unsubscribe" { - resp.Result = "true" - return - } - assert.Equal(t, "eth_subscribe", method) - if assert.True(t, params.IsArray()) && assert.Equal(t, "newHeads", params.Array()[0].String()) { - resp.Result = `"0x00"` - resp.Notify = headResult - } - return - }).WSURL().String() - - clients := mustNewClientsWithChainID(t, wsURL, chainId) - for _, ethClient := range clients { - err := ethClient.Dial(tests.Context(t)) - require.NoError(t, err) - - headCh := make(chan *evmtypes.Head) - sub, err := ethClient.SubscribeNewHead(ctx, headCh) - require.NoError(t, err) - - select { - case err := <-sub.Err(): - t.Fatal(err) - case <-ctx.Done(): - t.Fatal(ctx.Err()) - case h := <-headCh: - require.NotNil(t, h.EVMChainID) - require.Zero(t, chainId.Cmp(h.EVMChainID.ToInt())) - } - sub.Unsubscribe() - } -} - -func TestEthClient_ErroringClient(t *testing.T) { - t.Parallel() - ctx := tests.Context(t) - - // Empty node means there are no active nodes to select from, causing client to always return error. - erroringClient := client.NewChainClientWithEmptyNode(t, commonclient.NodeSelectionModeRoundRobin, time.Second*0, time.Second*0, testutils.FixtureChainID) - - _, err := erroringClient.BalanceAt(ctx, common.Address{}, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - err = erroringClient.BatchCallContext(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - err = erroringClient.BatchCallContextAll(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.BlockByHash(ctx, common.Hash{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.BlockByNumber(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - err = erroringClient.CallContext(ctx, nil, "") - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.CallContract(ctx, ethereum.CallMsg{}, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - // TODO-1663: test actual ChainID() call once client.go is deprecated. - id, err := erroringClient.ChainID() - require.Equal(t, id, testutils.FixtureChainID) - //require.Equal(t, err, commonclient.ErroringNodeError) - require.Equal(t, err, nil) - - _, err = erroringClient.CodeAt(ctx, common.Address{}, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - id = erroringClient.ConfiguredChainID() - require.Equal(t, id, testutils.FixtureChainID) - - err = erroringClient.Dial(ctx) - require.ErrorContains(t, err, "no available nodes for chain") - - _, err = erroringClient.EstimateGas(ctx, ethereum.CallMsg{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.FilterLogs(ctx, ethereum.FilterQuery{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.HeaderByHash(ctx, common.Hash{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.HeaderByNumber(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.HeadByHash(ctx, common.Hash{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.HeadByNumber(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.LINKBalance(ctx, common.Address{}, common.Address{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.LatestBlockHeight(ctx) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.PendingCodeAt(ctx, common.Address{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.PendingNonceAt(ctx, common.Address{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - err = erroringClient.SendTransaction(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - code, err := erroringClient.SendTransactionReturnCode(ctx, nil, common.Address{}) - require.Equal(t, code, commonclient.Unknown) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.SequenceAt(ctx, common.Address{}, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.SubscribeNewHead(ctx, nil) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.SuggestGasPrice(ctx) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.SuggestGasTipCap(ctx) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.TokenBalance(ctx, common.Address{}, common.Address{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.TransactionByHash(ctx, common.Hash{}) - require.Equal(t, err, commonclient.ErroringNodeError) - - _, err = erroringClient.TransactionReceipt(ctx, common.Hash{}) - require.Equal(t, err, commonclient.ErroringNodeError) -} - -const headResult = client.HeadResult diff --git a/core/chains/evm/client/erroring_node.go b/core/chains/evm/client/erroring_node.go deleted file mode 100644 index 00e8465bca3..00000000000 --- a/core/chains/evm/client/erroring_node.go +++ /dev/null @@ -1,150 +0,0 @@ -package client - -import ( - "context" - "math/big" - - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - pkgerrors "github.com/pkg/errors" -) - -var _ Node = (*erroringNode)(nil) - -type erroringNode struct { - errMsg string -} - -func (e *erroringNode) UnsubscribeAllExceptAliveLoop() {} - -func (e *erroringNode) SubscribersCount() int32 { - return 0 -} - -func (e *erroringNode) ChainID() (chainID *big.Int) { return nil } - -func (e *erroringNode) Start(ctx context.Context) error { return pkgerrors.New(e.errMsg) } - -func (e *erroringNode) Close() error { return nil } - -func (e *erroringNode) Verify(ctx context.Context, expectedChainID *big.Int) (err error) { - return pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - return pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - return pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) SendTransaction(ctx context.Context, tx *types.Transaction) error { - return pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { - return 0, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { - return 0, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) BlockNumber(ctx context.Context) (uint64, error) { - return 0, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { - return 0, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) HeaderByNumber(_ context.Context, _ *big.Int) (*types.Header, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) HeaderByHash(_ context.Context, _ common.Hash) (*types.Header, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) { - return nil, pkgerrors.New(e.errMsg) -} - -func (e *erroringNode) String() string { - return "" -} - -func (e *erroringNode) State() NodeState { - return NodeStateUnreachable -} - -func (e *erroringNode) StateAndLatest() (NodeState, int64, *big.Int) { - return NodeStateUnreachable, -1, nil -} - -func (e *erroringNode) Order() int32 { - return 100 -} - -func (e *erroringNode) DeclareOutOfSync() {} -func (e *erroringNode) DeclareInSync() {} -func (e *erroringNode) DeclareUnreachable() {} -func (e *erroringNode) Name() string { return "" } -func (e *erroringNode) NodeStates() map[int32]string { return nil } diff --git a/core/chains/evm/client/erroring_node_test.go b/core/chains/evm/client/erroring_node_test.go deleted file mode 100644 index 70a555485f0..00000000000 --- a/core/chains/evm/client/erroring_node_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package client - -import ( - "testing" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -func TestErroringNode(t *testing.T) { - t.Parallel() - - ctx := testutils.Context(t) - n := &erroringNode{ - "boo", - } - - require.Nil(t, n.ChainID()) - err := n.Start(ctx) - require.Equal(t, n.errMsg, err.Error()) - - defer func() { assert.NoError(t, n.Close()) }() - - err = n.Verify(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - err = n.CallContext(ctx, nil, "") - require.Equal(t, n.errMsg, err.Error()) - - err = n.BatchCallContext(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - err = n.SendTransaction(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.PendingCodeAt(ctx, common.Address{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.PendingNonceAt(ctx, common.Address{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.NonceAt(ctx, common.Address{}, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.TransactionReceipt(ctx, common.Hash{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.BlockByNumber(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.BlockByHash(ctx, common.Hash{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.BalanceAt(ctx, common.Address{}, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.FilterLogs(ctx, ethereum.FilterQuery{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.SubscribeFilterLogs(ctx, ethereum.FilterQuery{}, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.EstimateGas(ctx, ethereum.CallMsg{}) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.SuggestGasPrice(ctx) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.CallContract(ctx, ethereum.CallMsg{}, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.CodeAt(ctx, common.Address{}, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.HeaderByNumber(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.SuggestGasTipCap(ctx) - require.Equal(t, n.errMsg, err.Error()) - - _, err = n.EthSubscribe(ctx, nil) - require.Equal(t, n.errMsg, err.Error()) - - require.Equal(t, "", n.String()) - require.Equal(t, NodeStateUnreachable, n.State()) - - state, num, _ := n.StateAndLatest() - require.Equal(t, NodeStateUnreachable, state) - require.Equal(t, int64(-1), num) - - n.DeclareInSync() - n.DeclareOutOfSync() - n.DeclareUnreachable() - - require.Zero(t, n.Name()) - require.Nil(t, n.NodeStates()) -} diff --git a/core/chains/evm/client/errors.go b/core/chains/evm/client/errors.go index 5f03ab102d5..f43b5189c7e 100644 --- a/core/chains/evm/client/errors.go +++ b/core/chains/evm/client/errors.go @@ -149,7 +149,7 @@ var erigon = ClientErrors{ var arbitrumFatal = regexp.MustCompile(`(: |^)(invalid message format|forbidden sender address)$|(: |^)(execution reverted)(:|$)`) var arbitrum = ClientErrors{ // TODO: Arbitrum returns this in case of low or high nonce. Update this when Arbitrum fix it - // https://app.shortcut.com/chainlinklabs/story/16801/add-full-support-for-incorrect-nonce-on-arbitrum + // Archived ticket: story/16801/add-full-support-for-incorrect-nonce-on-arbitrum NonceTooLow: regexp.MustCompile(`(: |^)invalid transaction nonce$|(: |^)nonce too low(:|$)`), NonceTooHigh: regexp.MustCompile(`(: |^)nonce too high(:|$)`), TerminallyUnderpriced: regexp.MustCompile(`(: |^)gas price too low$`), diff --git a/core/chains/evm/client/helpers_test.go b/core/chains/evm/client/helpers_test.go index 7e2771a67d5..0fd33041896 100644 --- a/core/chains/evm/client/helpers_test.go +++ b/core/chains/evm/client/helpers_test.go @@ -109,40 +109,6 @@ func (tc TestNodePoolConfig) Errors() config.ClientErrors { return tc.NodeErrors } -func NewClientWithTestNode(t *testing.T, nodePoolCfg config.NodePool, noNewHeadsThreshold time.Duration, rpcUrl string, rpcHTTPURL *url.URL, sendonlyRPCURLs []url.URL, id int32, chainID *big.Int) (*client, error) { - parsed, err := url.ParseRequestURI(rpcUrl) - if err != nil { - return nil, err - } - - if parsed.Scheme != "ws" && parsed.Scheme != "wss" { - return nil, pkgerrors.Errorf("ethereum url scheme must be websocket: %s", parsed.String()) - } - - lggr := logger.Sugared(logger.Test(t)) - n := NewNode(nodePoolCfg, noNewHeadsThreshold, lggr, *parsed, rpcHTTPURL, "eth-primary-0", id, chainID, 1) - n.(*node).setLatestReceived(0, big.NewInt(0)) - primaries := []Node{n} - - var sendonlys []SendOnlyNode - for i, url := range sendonlyRPCURLs { - if url.Scheme != "http" && url.Scheme != "https" { - return nil, pkgerrors.Errorf("sendonly ethereum rpc url scheme must be http(s): %s", url.String()) - } - s := NewSendOnlyNode(lggr, url, fmt.Sprintf("eth-sendonly-%d", i), chainID) - sendonlys = append(sendonlys, s) - } - - pool := NewPool(lggr, nodePoolCfg.SelectionMode(), nodePoolCfg.LeaseDuration(), noNewHeadsThreshold, primaries, sendonlys, chainID, "") - c := &client{logger: lggr, pool: pool} - t.Cleanup(c.Close) - return c, nil -} - -func Wrap(err error, s string) error { - return wrap(err, s) -} - func NewChainClientWithTestNode( t *testing.T, nodeCfg commonclient.NodeConfig, @@ -217,7 +183,7 @@ func NewChainClientWithMockedRpc( var chainType commonconfig.ChainType cfg := TestNodePoolConfig{ - NodeSelectionMode: NodeSelectionMode_RoundRobin, + NodeSelectionMode: commonclient.NodeSelectionModeRoundRobin, } parsed, _ := url.ParseRequestURI("ws://test") @@ -230,17 +196,8 @@ func NewChainClientWithMockedRpc( return c } -type TestableSendOnlyNode interface { - SendOnlyNode - SetEthClient(newBatchSender BatchSender, newSender TxSender) -} - const HeadResult = `{"difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"0x1","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}` -func IsDialed(s SendOnlyNode) bool { - return s.(*sendOnlyNode).dialed -} - type mockSubscription struct { unsubscribed bool Errors chan error diff --git a/core/chains/evm/client/node.go b/core/chains/evm/client/node.go deleted file mode 100644 index 968cb34b9fe..00000000000 --- a/core/chains/evm/client/node.go +++ /dev/null @@ -1,1160 +0,0 @@ -package client - -import ( - "context" - "fmt" - "math/big" - "net/url" - "strconv" - "sync" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/rpc" - "github.com/google/uuid" - pkgerrors "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" - - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" -) - -var ( - promEVMPoolRPCNodeDials = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_dials_total", - Help: "The total number of dials for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeDialsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_dials_failed", - Help: "The total number of failed dials for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeDialsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_dials_success", - Help: "The total number of successful dials for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeVerifies = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_verifies", - Help: "The total number of chain ID verifications for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeVerifiesFailed = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_verifies_failed", - Help: "The total number of failed chain ID verifications for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeVerifiesSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_verifies_success", - Help: "The total number of successful chain ID verifications for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - - promEVMPoolRPCNodeCalls = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_calls_total", - Help: "The approximate total number of RPC calls for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeCallsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_calls_failed", - Help: "The approximate total number of failed RPC calls for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeCallsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_calls_success", - Help: "The approximate total number of successful RPC calls for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCCallTiming = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "evm_pool_rpc_node_rpc_call_time", - Help: "The duration of an RPC call in nanoseconds", - Buckets: []float64{ - float64(50 * time.Millisecond), - float64(100 * time.Millisecond), - float64(200 * time.Millisecond), - float64(500 * time.Millisecond), - float64(1 * time.Second), - float64(2 * time.Second), - float64(4 * time.Second), - float64(8 * time.Second), - }, - }, []string{"evmChainID", "nodeName", "rpcHost", "isSendOnly", "success", "rpcCallName"}) -) - -//go:generate mockery --quiet --name Node --output ../mocks/ --case=underscore - -// Node represents a client that connects to an ethereum-compatible RPC node -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.Node] -type Node interface { - Start(ctx context.Context) error - Close() error - - // State returns NodeState - State() NodeState - // StateAndLatest returns NodeState with the latest received block number & total difficulty. - StateAndLatest() (state NodeState, blockNum int64, totalDifficulty *big.Int) - // Name is a unique identifier for this node. - Name() string - ChainID() *big.Int - Order() int32 - SubscribersCount() int32 - UnsubscribeAllExceptAliveLoop() - - CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error - BatchCallContext(ctx context.Context, b []rpc.BatchElem) error - SendTransaction(ctx context.Context, tx *types.Transaction) error - PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) - PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) - NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) - TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) - TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) - BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) - BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) - BlockNumber(ctx context.Context) (uint64, error) - BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) - FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) - SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) - EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) - SuggestGasPrice(ctx context.Context) (*big.Int, error) - CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) - PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) - CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) - HeaderByNumber(context.Context, *big.Int) (*types.Header, error) - HeaderByHash(context.Context, common.Hash) (*types.Header, error) - SuggestGasTipCap(ctx context.Context) (*big.Int, error) - EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) - - String() string -} - -type rawclient struct { - rpc *rpc.Client - geth *ethclient.Client - uri url.URL -} - -// Node represents one ethereum node. -// It must have a ws url and may have a http url -type node struct { - services.StateMachine - lfcLog logger.Logger - rpcLog logger.SugaredLogger - name string - id int32 - chainID *big.Int - nodePoolCfg config.NodePool - noNewHeadsThreshold time.Duration - order int32 - - ws rawclient - http *rawclient - - stateMu sync.RWMutex // protects state* fields - state NodeState - // Each node is tracking the last received head number and total difficulty - stateLatestBlockNumber int64 - stateLatestTotalDifficulty *big.Int - - // Need to track subscriptions because closing the RPC does not (always?) - // close the underlying subscription - subs []ethereum.Subscription - - // Need to track the aliveLoop subscription, so we do not cancel it when checking lease - aliveLoopSub ethereum.Subscription - - // chStopInFlight can be closed to immediately cancel all in-flight requests on - // this node. Closing and replacing should be serialized through - // stateMu since it can happen on state transitions as well as node Close. - chStopInFlight chan struct{} - - stopCh services.StopChan - // wg waits for subsidiary goroutines - wg sync.WaitGroup - - // nLiveNodes is a passed in function that allows this node to: - // 1. see how many live nodes there are in total, so we can prevent the last alive node in a pool from being - // moved to out-of-sync state. It is better to have one out-of-sync node than no nodes at all. - // 2. compare against the highest head (by number or difficulty) to ensure we don't fall behind too far. - nLiveNodes func() (count int, blockNumber int64, totalDifficulty *big.Int) -} - -// NewNode returns a new *node as Node -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewNode] -func NewNode(nodeCfg config.NodePool, noNewHeadsThreshold time.Duration, lggr logger.Logger, wsuri url.URL, httpuri *url.URL, name string, id int32, chainID *big.Int, nodeOrder int32) Node { - n := new(node) - n.name = name - n.id = id - n.chainID = chainID - n.nodePoolCfg = nodeCfg - n.noNewHeadsThreshold = noNewHeadsThreshold - n.ws.uri = wsuri - n.order = nodeOrder - if httpuri != nil { - n.http = &rawclient{uri: *httpuri} - } - n.chStopInFlight = make(chan struct{}) - n.stopCh = make(chan struct{}) - lggr = logger.Named(lggr, "Node") - lggr = logger.With(lggr, - "nodeTier", "primary", - "nodeName", name, - "node", n.String(), - "evmChainID", chainID, - "nodeOrder", n.order, - "mode", n.getNodeMode(), - ) - n.lfcLog = logger.Named(lggr, "Lifecycle") - n.rpcLog = logger.Sugared(lggr).Named("RPC") - n.stateLatestBlockNumber = -1 - - return n -} - -// Start dials and verifies the node -// Should only be called once in a node's lifecycle -// Return value is necessary to conform to interface but this will never -// actually return an error. -func (n *node) Start(startCtx context.Context) error { - return n.StartOnce(n.name, func() error { - n.start(startCtx) - return nil - }) -} - -// start initially dials the node and verifies chain ID -// This spins off lifecycle goroutines. -// Not thread-safe. -// Node lifecycle is synchronous: only one goroutine should be running at a -// time. -func (n *node) start(startCtx context.Context) { - if n.state != NodeStateUndialed { - panic(fmt.Sprintf("cannot dial node with state %v", n.state)) - } - - dialCtx, dialCancel := n.makeQueryCtx(startCtx) - defer dialCancel() - if err := n.dial(dialCtx); err != nil { - n.lfcLog.Errorw("Dial failed: EVM Node is unreachable", "err", err) - n.declareUnreachable() - return - } - n.setState(NodeStateDialed) - - verifyCtx, verifyCancel := n.makeQueryCtx(startCtx) - defer verifyCancel() - if err := n.verify(verifyCtx); pkgerrors.Is(err, errInvalidChainID) { - n.lfcLog.Errorw("Verify failed: EVM Node has the wrong chain ID", "err", err) - n.declareInvalidChainID() - return - } else if err != nil { - n.lfcLog.Errorw(fmt.Sprintf("Verify failed: %v", err), "err", err) - n.declareUnreachable() - return - } - - n.declareAlive() -} - -// Not thread-safe -// Pure dial: does not mutate node "state" field. -func (n *node) dial(callerCtx context.Context) error { - ctx, cancel := n.makeQueryCtx(callerCtx) - defer cancel() - - promEVMPoolRPCNodeDials.WithLabelValues(n.chainID.String(), n.name).Inc() - lggr := logger.With(n.lfcLog, "wsuri", n.ws.uri.Redacted()) - if n.http != nil { - lggr = logger.With(lggr, "httpuri", n.http.uri.Redacted()) - } - lggr.Debugw("RPC dial: evmclient.Client#dial") - - wsrpc, err := rpc.DialWebsocket(ctx, n.ws.uri.String(), "") - if err != nil { - promEVMPoolRPCNodeDialsFailed.WithLabelValues(n.chainID.String(), n.name).Inc() - return pkgerrors.Wrapf(err, "error while dialing websocket: %v", n.ws.uri.Redacted()) - } - - var httprpc *rpc.Client - if n.http != nil { - httprpc, err = rpc.DialHTTP(n.http.uri.String()) - if err != nil { - promEVMPoolRPCNodeDialsFailed.WithLabelValues(n.chainID.String(), n.name).Inc() - return pkgerrors.Wrapf(err, "error while dialing HTTP: %v", n.http.uri.Redacted()) - } - } - - n.ws.rpc = wsrpc - n.ws.geth = ethclient.NewClient(wsrpc) - - if n.http != nil { - n.http.rpc = httprpc - n.http.geth = ethclient.NewClient(httprpc) - } - - promEVMPoolRPCNodeDialsSuccess.WithLabelValues(n.chainID.String(), n.name).Inc() - - return nil -} - -var errInvalidChainID = pkgerrors.New("invalid chain id") - -// verify checks that all connections to eth nodes match the given chain ID -// Not thread-safe -// Pure verify: does not mutate node "state" field. -func (n *node) verify(callerCtx context.Context) (err error) { - ctx, cancel := n.makeQueryCtx(callerCtx) - defer cancel() - - promEVMPoolRPCNodeVerifies.WithLabelValues(n.chainID.String(), n.name).Inc() - promFailed := func() { - promEVMPoolRPCNodeVerifiesFailed.WithLabelValues(n.chainID.String(), n.name).Inc() - } - - st := n.State() - switch st { - case NodeStateDialed, NodeStateOutOfSync, NodeStateInvalidChainID: - default: - panic(fmt.Sprintf("cannot verify node in state %v", st)) - } - - var chainID *big.Int - if chainID, err = n.ws.geth.ChainID(ctx); err != nil { - promFailed() - return pkgerrors.Wrapf(err, "failed to verify chain ID for node %s", n.name) - } else if chainID.Cmp(n.chainID) != 0 { - promFailed() - return pkgerrors.Wrapf( - errInvalidChainID, - "websocket rpc ChainID doesn't match local chain ID: RPC ID=%s, local ID=%s, node name=%s", - chainID.String(), - n.chainID.String(), - n.name, - ) - } - if n.http != nil { - if chainID, err = n.http.geth.ChainID(ctx); err != nil { - promFailed() - return pkgerrors.Wrapf(err, "failed to verify chain ID for node %s", n.name) - } else if chainID.Cmp(n.chainID) != 0 { - promFailed() - return pkgerrors.Wrapf( - errInvalidChainID, - "http rpc ChainID doesn't match local chain ID: RPC ID=%s, local ID=%s, node name=%s", - chainID.String(), - n.chainID.String(), - n.name, - ) - } - } - - promEVMPoolRPCNodeVerifiesSuccess.WithLabelValues(n.chainID.String(), n.name).Inc() - - return nil -} - -func (n *node) Close() error { - return n.StopOnce(n.name, func() error { - defer func() { - n.wg.Wait() - if n.ws.rpc != nil { - n.ws.rpc.Close() - } - }() - - n.stateMu.Lock() - defer n.stateMu.Unlock() - - close(n.stopCh) - n.cancelInflightRequests() - n.state = NodeStateClosed - return nil - }) -} - -// registerSub adds the sub to the node list -func (n *node) registerSub(sub ethereum.Subscription) { - n.stateMu.Lock() - defer n.stateMu.Unlock() - n.subs = append(n.subs, sub) -} - -// disconnectAll disconnects all clients connected to the node -// WARNING: NOT THREAD-SAFE -// This must be called from within the n.stateMu lock -func (n *node) disconnectAll() { - if n.ws.rpc != nil { - n.ws.rpc.Close() - } - n.cancelInflightRequests() - n.unsubscribeAll() -} - -// SubscribersCount returns the number of client subscribed to the node -func (n *node) SubscribersCount() int32 { - n.stateMu.RLock() - defer n.stateMu.RUnlock() - return int32(len(n.subs)) -} - -// UnsubscribeAllExceptAliveLoop disconnects all subscriptions to the node except the alive loop subscription -// while holding the n.stateMu lock -func (n *node) UnsubscribeAllExceptAliveLoop() { - n.stateMu.Lock() - defer n.stateMu.Unlock() - - for _, s := range n.subs { - if s != n.aliveLoopSub { - s.Unsubscribe() - } - } -} - -// cancelInflightRequests closes and replaces the chStopInFlight -// WARNING: NOT THREAD-SAFE -// This must be called from within the n.stateMu lock -func (n *node) cancelInflightRequests() { - close(n.chStopInFlight) - n.chStopInFlight = make(chan struct{}) -} - -// unsubscribeAll unsubscribes all subscriptions -// WARNING: NOT THREAD-SAFE -// This must be called from within the n.stateMu lock -func (n *node) unsubscribeAll() { - for _, sub := range n.subs { - sub.Unsubscribe() - } - n.subs = nil -} - -// getChStopInflight provides a convenience helper that mutex wraps a -// read to the chStopInFlight -func (n *node) getChStopInflight() chan struct{} { - n.stateMu.RLock() - defer n.stateMu.RUnlock() - return n.chStopInFlight -} - -func (n *node) getRPCDomain() string { - if n.http != nil { - return n.http.uri.Host - } - return n.ws.uri.Host -} - -// RPC wrappers - -// CallContext implementation -func (n *node) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return err - } - defer cancel() - lggr := n.newRqLggr().With( - "method", method, - "args", args, - ) - - lggr.Debug("RPC call: evmclient.Client#CallContext") - start := time.Now() - if http != nil { - err = n.wrapHTTP(http.rpc.CallContext(ctx, result, method, args...)) - } else { - err = n.wrapWS(ws.rpc.CallContext(ctx, result, method, args...)) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "CallContext") - - return err -} - -func (n *node) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return err - } - defer cancel() - lggr := n.newRqLggr().With("nBatchElems", len(b), "batchElems", b) - - lggr.Trace("RPC call: evmclient.Client#BatchCallContext") - start := time.Now() - if http != nil { - err = n.wrapHTTP(http.rpc.BatchCallContext(ctx, b)) - } else { - err = n.wrapWS(ws.rpc.BatchCallContext(ctx, b)) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "BatchCallContext") - - return err -} - -func (n *node) EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) { - ctx, cancel, ws, _, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("args", args) - - lggr.Debug("RPC call: evmclient.Client#EthSubscribe") - start := time.Now() - sub, err := ws.rpc.EthSubscribe(ctx, channel, args...) - if err == nil { - n.registerSub(sub) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "EthSubscribe") - - return sub, err -} - -// GethClient wrappers - -func (n *node) TransactionReceipt(ctx context.Context, txHash common.Hash) (receipt *types.Receipt, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("txHash", txHash) - - lggr.Debug("RPC call: evmclient.Client#TransactionReceipt") - - start := time.Now() - if http != nil { - receipt, err = http.geth.TransactionReceipt(ctx, txHash) - err = n.wrapHTTP(err) - } else { - receipt, err = ws.geth.TransactionReceipt(ctx, txHash) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "TransactionReceipt", - "receipt", receipt, - ) - - return -} - -func (n *node) TransactionByHash(ctx context.Context, txHash common.Hash) (tx *types.Transaction, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("txHash", txHash) - - lggr.Debug("RPC call: evmclient.Client#TransactionByHash") - - start := time.Now() - if http != nil { - tx, _, err = http.geth.TransactionByHash(ctx, txHash) - err = n.wrapHTTP(err) - } else { - tx, _, err = ws.geth.TransactionByHash(ctx, txHash) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "TransactionByHash", - "receipt", tx, - ) - - return -} - -func (n *node) HeaderByNumber(ctx context.Context, number *big.Int) (header *types.Header, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("number", number) - - lggr.Debug("RPC call: evmclient.Client#HeaderByNumber") - start := time.Now() - if http != nil { - header, err = http.geth.HeaderByNumber(ctx, number) - err = n.wrapHTTP(err) - } else { - header, err = ws.geth.HeaderByNumber(ctx, number) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "HeaderByNumber", "header", header) - - return -} - -func (n *node) HeaderByHash(ctx context.Context, hash common.Hash) (header *types.Header, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("hash", hash) - - lggr.Debug("RPC call: evmclient.Client#HeaderByHash") - start := time.Now() - if http != nil { - header, err = http.geth.HeaderByHash(ctx, hash) - err = n.wrapHTTP(err) - } else { - header, err = ws.geth.HeaderByHash(ctx, hash) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "HeaderByHash", - "header", header, - ) - - return -} - -func (n *node) SendTransaction(ctx context.Context, tx *types.Transaction) error { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return err - } - defer cancel() - lggr := n.newRqLggr().With("tx", tx) - - lggr.Debug("RPC call: evmclient.Client#SendTransaction") - start := time.Now() - if http != nil { - err = n.wrapHTTP(http.geth.SendTransaction(ctx, tx)) - } else { - err = n.wrapWS(ws.geth.SendTransaction(ctx, tx)) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "SendTransaction") - - return err -} - -// PendingNonceAt returns one higher than the highest nonce from both mempool and mined transactions -func (n *node) PendingNonceAt(ctx context.Context, account common.Address) (nonce uint64, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return 0, err - } - defer cancel() - lggr := n.newRqLggr().With("account", account) - - lggr.Debug("RPC call: evmclient.Client#PendingNonceAt") - start := time.Now() - if http != nil { - nonce, err = http.geth.PendingNonceAt(ctx, account) - err = n.wrapHTTP(err) - } else { - nonce, err = ws.geth.PendingNonceAt(ctx, account) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "PendingNonceAt", - "nonce", nonce, - ) - - return -} - -// NonceAt is a bit of a misnomer. You might expect it to return the highest -// mined nonce at the given block number, but it actually returns the total -// transaction count which is the highest mined nonce + 1 -func (n *node) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (nonce uint64, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return 0, err - } - defer cancel() - lggr := n.newRqLggr().With("account", account, "blockNumber", blockNumber) - - lggr.Debug("RPC call: evmclient.Client#NonceAt") - start := time.Now() - if http != nil { - nonce, err = http.geth.NonceAt(ctx, account, blockNumber) - err = n.wrapHTTP(err) - } else { - nonce, err = ws.geth.NonceAt(ctx, account, blockNumber) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "NonceAt", - "nonce", nonce, - ) - - return -} - -func (n *node) PendingCodeAt(ctx context.Context, account common.Address) (code []byte, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("account", account) - - lggr.Debug("RPC call: evmclient.Client#PendingCodeAt") - start := time.Now() - if http != nil { - code, err = http.geth.PendingCodeAt(ctx, account) - err = n.wrapHTTP(err) - } else { - code, err = ws.geth.PendingCodeAt(ctx, account) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "PendingCodeAt", - "code", code, - ) - - return -} - -func (n *node) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) (code []byte, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("account", account, "blockNumber", blockNumber) - - lggr.Debug("RPC call: evmclient.Client#CodeAt") - start := time.Now() - if http != nil { - code, err = http.geth.CodeAt(ctx, account, blockNumber) - err = n.wrapHTTP(err) - } else { - code, err = ws.geth.CodeAt(ctx, account, blockNumber) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "CodeAt", - "code", code, - ) - - return -} - -func (n *node) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return 0, err - } - defer cancel() - lggr := n.newRqLggr().With("call", call) - - lggr.Debug("RPC call: evmclient.Client#EstimateGas") - start := time.Now() - if http != nil { - gas, err = http.geth.EstimateGas(ctx, call) - err = n.wrapHTTP(err) - } else { - gas, err = ws.geth.EstimateGas(ctx, call) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "EstimateGas", - "gas", gas, - ) - - return -} - -func (n *node) SuggestGasPrice(ctx context.Context) (price *big.Int, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr() - - lggr.Debug("RPC call: evmclient.Client#SuggestGasPrice") - start := time.Now() - if http != nil { - price, err = http.geth.SuggestGasPrice(ctx) - err = n.wrapHTTP(err) - } else { - price, err = ws.geth.SuggestGasPrice(ctx) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "SuggestGasPrice", - "price", price, - ) - - return -} - -func (n *node) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) (val []byte, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("callMsg", msg, "blockNumber", blockNumber) - - lggr.Debug("RPC call: evmclient.Client#CallContract") - start := time.Now() - if http != nil { - val, err = http.geth.CallContract(ctx, msg, blockNumber) - err = n.wrapHTTP(err) - } else { - val, err = ws.geth.CallContract(ctx, msg, blockNumber) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "CallContract", - "val", val, - ) - - return -} - -func (n *node) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) (val []byte, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("callMsg", msg) - - lggr.Debug("RPC call: evmclient.Client#PendingCallContract") - start := time.Now() - if http != nil { - val, err = http.geth.PendingCallContract(ctx, msg) - err = n.wrapHTTP(err) - } else { - val, err = ws.geth.PendingCallContract(ctx, msg) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "PendingCallContract", - "val", val, - ) - - return -} - -func (n *node) BlockByNumber(ctx context.Context, number *big.Int) (b *types.Block, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("number", number) - - lggr.Debug("RPC call: evmclient.Client#BlockByNumber") - start := time.Now() - if http != nil { - b, err = http.geth.BlockByNumber(ctx, number) - err = n.wrapHTTP(err) - } else { - b, err = ws.geth.BlockByNumber(ctx, number) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "BlockByNumber", - "block", b, - ) - - return -} - -func (n *node) BlockByHash(ctx context.Context, hash common.Hash) (b *types.Block, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("hash", hash) - - lggr.Debug("RPC call: evmclient.Client#BlockByHash") - start := time.Now() - if http != nil { - b, err = http.geth.BlockByHash(ctx, hash) - err = n.wrapHTTP(err) - } else { - b, err = ws.geth.BlockByHash(ctx, hash) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "BlockByHash", - "block", b, - ) - - return -} - -func (n *node) BlockNumber(ctx context.Context) (height uint64, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return 0, err - } - defer cancel() - lggr := n.newRqLggr() - - lggr.Debug("RPC call: evmclient.Client#BlockNumber") - start := time.Now() - if http != nil { - height, err = http.geth.BlockNumber(ctx) - err = n.wrapHTTP(err) - } else { - height, err = ws.geth.BlockNumber(ctx) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "BlockNumber", - "height", height, - ) - - return -} - -func (n *node) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (balance *big.Int, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("account", account.Hex(), "blockNumber", blockNumber) - - lggr.Debug("RPC call: evmclient.Client#BalanceAt") - start := time.Now() - if http != nil { - balance, err = http.geth.BalanceAt(ctx, account, blockNumber) - err = n.wrapHTTP(err) - } else { - balance, err = ws.geth.BalanceAt(ctx, account, blockNumber) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "BalanceAt", - "balance", balance, - ) - - return -} - -func (n *node) FilterLogs(ctx context.Context, q ethereum.FilterQuery) (l []types.Log, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("q", q) - - lggr.Debug("RPC call: evmclient.Client#FilterLogs") - start := time.Now() - if http != nil { - l, err = http.geth.FilterLogs(ctx, q) - err = n.wrapHTTP(err) - } else { - l, err = ws.geth.FilterLogs(ctx, q) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "FilterLogs", - "log", l, - ) - - return -} - -func (n *node) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (sub ethereum.Subscription, err error) { - ctx, cancel, ws, _, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr().With("q", q) - - lggr.Debug("RPC call: evmclient.Client#SubscribeFilterLogs") - start := time.Now() - sub, err = ws.geth.SubscribeFilterLogs(ctx, q, ch) - if err == nil { - n.registerSub(sub) - } - err = n.wrapWS(err) - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "SubscribeFilterLogs") - - return -} - -func (n *node) SuggestGasTipCap(ctx context.Context) (tipCap *big.Int, err error) { - ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) - if err != nil { - return nil, err - } - defer cancel() - lggr := n.newRqLggr() - - lggr.Debug("RPC call: evmclient.Client#SuggestGasTipCap") - start := time.Now() - if http != nil { - tipCap, err = http.geth.SuggestGasTipCap(ctx) - err = n.wrapHTTP(err) - } else { - tipCap, err = ws.geth.SuggestGasTipCap(ctx) - err = n.wrapWS(err) - } - duration := time.Since(start) - - n.logResult(lggr, err, duration, n.getRPCDomain(), "SuggestGasTipCap", - "tipCap", tipCap, - ) - - return -} - -func (n *node) ChainID() (chainID *big.Int) { return n.chainID } - -// newRqLggr generates a new logger with a unique request ID -func (n *node) newRqLggr() logger.SugaredLogger { - return n.rpcLog.With("requestID", uuid.New()) -} - -func (n *node) logResult( - lggr logger.Logger, - err error, - callDuration time.Duration, - rpcDomain, - callName string, - results ...interface{}, -) { - slggr := logger.Sugared(lggr).With("duration", callDuration, "rpcDomain", rpcDomain, "callName", callName) - promEVMPoolRPCNodeCalls.WithLabelValues(n.chainID.String(), n.name).Inc() - if err == nil { - promEVMPoolRPCNodeCallsSuccess.WithLabelValues(n.chainID.String(), n.name).Inc() - slggr.Tracew(fmt.Sprintf("evmclient.Client#%s RPC call success", callName), results...) - } else { - promEVMPoolRPCNodeCallsFailed.WithLabelValues(n.chainID.String(), n.name).Inc() - slggr.Debugw( - fmt.Sprintf("evmclient.Client#%s RPC call failure", callName), - append(results, "err", err)..., - ) - } - promEVMPoolRPCCallTiming. - WithLabelValues( - n.chainID.String(), // chain id - n.name, // node name - rpcDomain, // rpc domain - "false", // is send only - strconv.FormatBool(err == nil), // is successful - callName, // rpc call name - ). - Observe(float64(callDuration)) -} - -func (n *node) wrapWS(err error) error { - err = wrap(err, fmt.Sprintf("primary websocket (%s)", n.ws.uri.Redacted())) - return err -} - -func (n *node) wrapHTTP(err error) error { - err = wrap(err, fmt.Sprintf("primary http (%s)", n.http.uri.Redacted())) - if err != nil { - n.rpcLog.Debugw("Call failed", "err", err) - } else { - n.rpcLog.Trace("Call succeeded") - } - return err -} - -func wrap(err error, tp string) error { - if err == nil { - return nil - } - if pkgerrors.Cause(err).Error() == "context deadline exceeded" { - err = pkgerrors.Wrap(err, "remote eth node timed out") - } - return pkgerrors.Wrapf(err, "%s call failed", tp) -} - -// makeLiveQueryCtxAndSafeGetClients wraps makeQueryCtx but returns error if node is not NodeStateAlive. -func (n *node) makeLiveQueryCtxAndSafeGetClients(parentCtx context.Context) (ctx context.Context, cancel context.CancelFunc, ws rawclient, http *rawclient, err error) { - // Need to wrap in mutex because state transition can cancel and replace the - // context - n.stateMu.RLock() - if n.state != NodeStateAlive { - err = pkgerrors.Errorf("cannot execute RPC call on node with state: %s", n.state) - n.stateMu.RUnlock() - return - } - cancelCh := n.chStopInFlight - ws = n.ws - if n.http != nil { - cp := *n.http - http = &cp - } - n.stateMu.RUnlock() - ctx, cancel = makeQueryCtx(parentCtx, cancelCh) - return -} - -func (n *node) makeQueryCtx(ctx context.Context) (context.Context, context.CancelFunc) { - return makeQueryCtx(ctx, n.getChStopInflight()) -} - -// makeQueryCtx returns a context that cancels if: -// 1. Passed in ctx cancels -// 2. Passed in channel is closed -// 3. Default timeout is reached (queryTimeout) -func makeQueryCtx(ctx context.Context, ch services.StopChan) (context.Context, context.CancelFunc) { - var chCancel, timeoutCancel context.CancelFunc - ctx, chCancel = ch.Ctx(ctx) - ctx, timeoutCancel = context.WithTimeout(ctx, queryTimeout) - cancel := func() { - chCancel() - timeoutCancel() - } - return ctx, cancel -} - -func (n *node) getNodeMode() string { - if n.http != nil { - return "http" - } - return "websocket" -} - -func (n *node) String() string { - s := fmt.Sprintf("(primary)%s:%s", n.name, n.ws.uri.Redacted()) - if n.http != nil { - s = s + fmt.Sprintf(":%s", n.http.uri.Redacted()) - } - return s -} - -func (n *node) Name() string { - return n.name -} - -func (n *node) Order() int32 { - return n.order -} diff --git a/core/chains/evm/client/node_fsm.go b/core/chains/evm/client/node_fsm.go deleted file mode 100644 index c92af3b4120..00000000000 --- a/core/chains/evm/client/node_fsm.go +++ /dev/null @@ -1,259 +0,0 @@ -package client - -import ( - "fmt" - "math/big" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" -) - -var ( - promEVMPoolRPCNodeTransitionsToAlive = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_alive", - Help: fmt.Sprintf("Total number of times node has transitioned to %s", NodeStateAlive), - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeTransitionsToInSync = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_in_sync", - Help: fmt.Sprintf("Total number of times node has transitioned from %s to %s", NodeStateOutOfSync, NodeStateAlive), - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeTransitionsToOutOfSync = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_out_of_sync", - Help: fmt.Sprintf("Total number of times node has transitioned to %s", NodeStateOutOfSync), - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeTransitionsToUnreachable = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_unreachable", - Help: fmt.Sprintf("Total number of times node has transitioned to %s", NodeStateUnreachable), - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeTransitionsToInvalidChainID = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_invalid_chain_id", - Help: fmt.Sprintf("Total number of times node has transitioned to %s", NodeStateInvalidChainID), - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeTransitionsToUnusable = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_transitions_to_unusable", - Help: fmt.Sprintf("Total number of times node has transitioned to %s", NodeStateUnusable), - }, []string{"evmChainID", "nodeName"}) -) - -// NodeState represents the current state of the node -// Node is a FSM (finite state machine) -// -// Deprecated: to be removed. It is now internal in common/client -type NodeState int - -func (n NodeState) String() string { - switch n { - case NodeStateUndialed: - return "Undialed" - case NodeStateDialed: - return "Dialed" - case NodeStateInvalidChainID: - return "InvalidChainID" - case NodeStateAlive: - return "Alive" - case NodeStateUnreachable: - return "Unreachable" - case NodeStateUnusable: - return "Unusable" - case NodeStateOutOfSync: - return "OutOfSync" - case NodeStateClosed: - return "Closed" - default: - return fmt.Sprintf("NodeState(%d)", n) - } -} - -// GoString prints a prettier state -func (n NodeState) GoString() string { - return fmt.Sprintf("NodeState%s(%d)", n.String(), n) -} - -const ( - // NodeStateUndialed is the first state of a virgin node - NodeStateUndialed = NodeState(iota) - // NodeStateDialed is after a node has successfully dialed but before it has verified the correct chain ID - NodeStateDialed - // NodeStateInvalidChainID is after chain ID verification failed - NodeStateInvalidChainID - // NodeStateAlive is a healthy node after chain ID verification succeeded - NodeStateAlive - // NodeStateUnreachable is a node that cannot be dialed or has disconnected - NodeStateUnreachable - // NodeStateOutOfSync is a node that is accepting connections but exceeded - // the failure threshold without sending any new heads. It will be - // disconnected, then put into a revive loop and re-awakened after redial - // if a new head arrives - NodeStateOutOfSync - // NodeStateUnusable is a sendonly node that has an invalid URL that can never be reached - NodeStateUnusable - // NodeStateClosed is after the connection has been closed and the node is at the end of its lifecycle - NodeStateClosed - // nodeStateLen tracks the number of states - nodeStateLen -) - -// allNodeStates represents all possible states a node can be in -var allNodeStates []NodeState - -func init() { - for s := NodeState(0); s < nodeStateLen; s++ { - allNodeStates = append(allNodeStates, s) - } -} - -// FSM methods - -// State allows reading the current state of the node. -func (n *node) State() NodeState { - n.stateMu.RLock() - defer n.stateMu.RUnlock() - return n.state -} - -func (n *node) StateAndLatest() (NodeState, int64, *big.Int) { - n.stateMu.RLock() - defer n.stateMu.RUnlock() - return n.state, n.stateLatestBlockNumber, n.stateLatestTotalDifficulty -} - -// setState is only used by internal state management methods. -// This is low-level; care should be taken by the caller to ensure the new state is a valid transition. -// State changes should always be synchronous: only one goroutine at a time should change state. -// n.stateMu should not be locked for long periods of time because external clients expect a timely response from n.State() -func (n *node) setState(s NodeState) { - n.stateMu.Lock() - defer n.stateMu.Unlock() - n.state = s -} - -// declareXXX methods change the state and pass conrol off the new state -// management goroutine - -func (n *node) declareAlive() { - n.transitionToAlive(func() { - n.lfcLog.Infow("RPC Node is online", "nodeState", n.state) - n.wg.Add(1) - go n.aliveLoop() - }) -} - -func (n *node) transitionToAlive(fn func()) { - promEVMPoolRPCNodeTransitionsToAlive.WithLabelValues(n.chainID.String(), n.name).Inc() - n.stateMu.Lock() - defer n.stateMu.Unlock() - if n.state == NodeStateClosed { - return - } - switch n.state { - case NodeStateDialed, NodeStateInvalidChainID: - n.state = NodeStateAlive - default: - panic(fmt.Sprintf("cannot transition from %#v to %#v", n.state, NodeStateAlive)) - } - fn() -} - -// declareInSync puts a node back into Alive state, allowing it to be used by -// pool consumers again -func (n *node) declareInSync() { - n.transitionToInSync(func() { - n.lfcLog.Infow("RPC Node is back in sync", "nodeState", n.state) - n.wg.Add(1) - go n.aliveLoop() - }) -} - -func (n *node) transitionToInSync(fn func()) { - promEVMPoolRPCNodeTransitionsToAlive.WithLabelValues(n.chainID.String(), n.name).Inc() - promEVMPoolRPCNodeTransitionsToInSync.WithLabelValues(n.chainID.String(), n.name).Inc() - n.stateMu.Lock() - defer n.stateMu.Unlock() - if n.state == NodeStateClosed { - return - } - switch n.state { - case NodeStateOutOfSync: - n.state = NodeStateAlive - default: - panic(fmt.Sprintf("cannot transition from %#v to %#v", n.state, NodeStateAlive)) - } - fn() -} - -// declareOutOfSync puts a node into OutOfSync state, disconnecting all current -// clients and making it unavailable for use until back in-sync. -func (n *node) declareOutOfSync(isOutOfSync func(num int64, td *big.Int) bool) { - n.transitionToOutOfSync(func() { - n.lfcLog.Errorw("RPC Node is out of sync", "nodeState", n.state) - n.wg.Add(1) - go n.outOfSyncLoop(isOutOfSync) - }) -} - -func (n *node) transitionToOutOfSync(fn func()) { - promEVMPoolRPCNodeTransitionsToOutOfSync.WithLabelValues(n.chainID.String(), n.name).Inc() - n.stateMu.Lock() - defer n.stateMu.Unlock() - if n.state == NodeStateClosed { - return - } - switch n.state { - case NodeStateAlive: - n.disconnectAll() - n.state = NodeStateOutOfSync - default: - panic(fmt.Sprintf("cannot transition from %#v to %#v", n.state, NodeStateOutOfSync)) - } - fn() -} - -func (n *node) declareUnreachable() { - n.transitionToUnreachable(func() { - n.lfcLog.Errorw("RPC Node is unreachable", "nodeState", n.state) - n.wg.Add(1) - go n.unreachableLoop() - }) -} - -func (n *node) transitionToUnreachable(fn func()) { - promEVMPoolRPCNodeTransitionsToUnreachable.WithLabelValues(n.chainID.String(), n.name).Inc() - n.stateMu.Lock() - defer n.stateMu.Unlock() - if n.state == NodeStateClosed { - return - } - switch n.state { - case NodeStateUndialed, NodeStateDialed, NodeStateAlive, NodeStateOutOfSync, NodeStateInvalidChainID: - n.disconnectAll() - n.state = NodeStateUnreachable - default: - panic(fmt.Sprintf("cannot transition from %#v to %#v", n.state, NodeStateUnreachable)) - } - fn() -} - -func (n *node) declareInvalidChainID() { - n.transitionToInvalidChainID(func() { - n.lfcLog.Errorw("RPC Node has the wrong chain ID", "nodeState", n.state) - n.wg.Add(1) - go n.invalidChainIDLoop() - }) -} - -func (n *node) transitionToInvalidChainID(fn func()) { - promEVMPoolRPCNodeTransitionsToInvalidChainID.WithLabelValues(n.chainID.String(), n.name).Inc() - n.stateMu.Lock() - defer n.stateMu.Unlock() - if n.state == NodeStateClosed { - return - } - switch n.state { - case NodeStateDialed, NodeStateOutOfSync: - n.disconnectAll() - n.state = NodeStateInvalidChainID - default: - panic(fmt.Sprintf("cannot transition from %#v to %#v", n.state, NodeStateInvalidChainID)) - } - fn() -} diff --git a/core/chains/evm/client/node_fsm_test.go b/core/chains/evm/client/node_fsm_test.go deleted file mode 100644 index 321bbc7a309..00000000000 --- a/core/chains/evm/client/node_fsm_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package client - -import ( - "testing" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -type fnMock struct{ calls int } - -func (fm *fnMock) Fn() { - fm.calls++ -} - -func (fm *fnMock) AssertNotCalled(t *testing.T) { - assert.Equal(t, 0, fm.calls) -} - -func (fm *fnMock) AssertCalled(t *testing.T) { - assert.Greater(t, fm.calls, 0) -} - -func (fm *fnMock) AssertNumberOfCalls(t *testing.T, n int) { - assert.Equal(t, n, fm.calls) -} - -var _ ethereum.Subscription = (*subMock)(nil) - -type subMock struct{ unsubbed bool } - -func (s *subMock) Unsubscribe() { - s.unsubbed = true -} -func (s *subMock) Err() <-chan error { return nil } - -func TestUnit_Node_StateTransitions(t *testing.T) { - t.Parallel() - - s := testutils.NewWSServer(t, testutils.FixtureChainID, nil) - iN := NewNode(TestNodePoolConfig{}, time.Second*0, logger.Test(t), *s.WSURL(), nil, "test node", 42, nil, 1) - n := iN.(*node) - - assert.Equal(t, NodeStateUndialed, n.State()) - - t.Run("setState", func(t *testing.T) { - n.setState(NodeStateAlive) - assert.Equal(t, NodeStateAlive, n.State()) - n.setState(NodeStateUndialed) - assert.Equal(t, NodeStateUndialed, n.State()) - }) - - // must dial to set rpc client for use in state transitions - err := n.dial(testutils.Context(t)) - require.NoError(t, err) - - t.Run("transitionToAlive", func(t *testing.T) { - m := new(fnMock) - assert.Panics(t, func() { - n.transitionToAlive(m.Fn) - }) - m.AssertNotCalled(t) - n.setState(NodeStateDialed) - n.transitionToAlive(m.Fn) - m.AssertNumberOfCalls(t, 1) - n.setState(NodeStateInvalidChainID) - n.transitionToAlive(m.Fn) - m.AssertNumberOfCalls(t, 2) - }) - - t.Run("transitionToInSync", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateAlive) - assert.Panics(t, func() { - n.transitionToInSync(m.Fn) - }) - m.AssertNotCalled(t) - n.setState(NodeStateOutOfSync) - n.transitionToInSync(m.Fn) - m.AssertCalled(t) - }) - t.Run("transitionToOutOfSync", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateOutOfSync) - assert.Panics(t, func() { - n.transitionToOutOfSync(m.Fn) - }) - m.AssertNotCalled(t) - n.setState(NodeStateAlive) - n.transitionToOutOfSync(m.Fn) - m.AssertCalled(t) - }) - t.Run("transitionToOutOfSync unsubscribes everything", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateAlive) - sub := &subMock{} - n.registerSub(sub) - n.transitionToOutOfSync(m.Fn) - m.AssertNumberOfCalls(t, 1) - assert.True(t, sub.unsubbed) - }) - t.Run("transitionToUnreachable", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateUnreachable) - assert.Panics(t, func() { - n.transitionToUnreachable(m.Fn) - }) - m.AssertNotCalled(t) - n.setState(NodeStateDialed) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 1) - n.setState(NodeStateAlive) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 2) - n.setState(NodeStateOutOfSync) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 3) - n.setState(NodeStateUndialed) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 4) - n.setState(NodeStateInvalidChainID) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 5) - }) - t.Run("transitionToUnreachable unsubscribes everything", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateDialed) - sub := &subMock{} - n.registerSub(sub) - n.transitionToUnreachable(m.Fn) - m.AssertNumberOfCalls(t, 1) - assert.True(t, sub.unsubbed) - }) - t.Run("transitionToInvalidChainID", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateUnreachable) - assert.Panics(t, func() { - n.transitionToInvalidChainID(m.Fn) - }) - m.AssertNotCalled(t) - n.setState(NodeStateDialed) - n.transitionToInvalidChainID(m.Fn) - n.setState(NodeStateOutOfSync) - n.transitionToInvalidChainID(m.Fn) - m.AssertNumberOfCalls(t, 2) - }) - t.Run("transitionToInvalidChainID unsubscribes everything", func(t *testing.T) { - m := new(fnMock) - n.setState(NodeStateDialed) - sub := &subMock{} - n.registerSub(sub) - n.transitionToInvalidChainID(m.Fn) - m.AssertNumberOfCalls(t, 1) - assert.True(t, sub.unsubbed) - }) - t.Run("Close", func(t *testing.T) { - // first attempt errors due to node being unstarted - assert.Error(t, n.Close()) - // must start to allow closing - err := n.StartOnce("test node", func() error { return nil }) - assert.NoError(t, err) - assert.NoError(t, n.Close()) - - assert.Equal(t, NodeStateClosed, n.State()) - // second attempt errors due to node being stopped twice - assert.Error(t, n.Close()) - }) -} diff --git a/core/chains/evm/client/node_lifecycle.go b/core/chains/evm/client/node_lifecycle.go deleted file mode 100644 index c18c8032009..00000000000 --- a/core/chains/evm/client/node_lifecycle.go +++ /dev/null @@ -1,451 +0,0 @@ -package client - -import ( - "context" - "fmt" - "math" - "math/big" - "time" - - pkgerrors "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - cutils "github.com/smartcontractkit/chainlink-common/pkg/utils" - bigmath "github.com/smartcontractkit/chainlink-common/pkg/utils/big_math" - - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" -) - -var ( - promEVMPoolRPCNodeHighestSeenBlock = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "evm_pool_rpc_node_highest_seen_block", - Help: "The highest seen block for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodeNumSeenBlocks = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_num_seen_blocks", - Help: "The total number of new blocks seen by the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodePolls = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_polls_total", - Help: "The total number of poll checks for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodePollsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_polls_failed", - Help: "The total number of failed poll checks for the given RPC node", - }, []string{"evmChainID", "nodeName"}) - promEVMPoolRPCNodePollsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "evm_pool_rpc_node_polls_success", - Help: "The total number of successful poll checks for the given RPC node", - }, []string{"evmChainID", "nodeName"}) -) - -// zombieNodeCheckInterval controls how often to re-check to see if we need to -// state change in case we have to force a state transition due to no available -// nodes. -// NOTE: This only applies to out-of-sync nodes if they are the last available node -func zombieNodeCheckInterval(noNewHeadsThreshold time.Duration) time.Duration { - interval := noNewHeadsThreshold - if interval <= 0 || interval > queryTimeout { - interval = queryTimeout - } - return cutils.WithJitter(interval) -} - -func (n *node) setLatestReceived(blockNumber int64, totalDifficulty *big.Int) { - n.stateMu.Lock() - defer n.stateMu.Unlock() - n.stateLatestBlockNumber = blockNumber - n.stateLatestTotalDifficulty = totalDifficulty -} - -const ( - msgCannotDisable = "but cannot disable this connection because there are no other RPC endpoints, or all other RPC endpoints are dead." - msgDegradedState = "Chainlink is now operating in a degraded state and urgent action is required to resolve the issue" -) - -// Node is a FSM -// Each state has a loop that goes with it, which monitors the node and moves it into another state as necessary. -// Only one loop must run at a time. -// Each loop passes control onto the next loop as it exits, except when the node is Closed which terminates the loop permanently. - -// This handles node lifecycle for the ALIVE state -// Should only be run ONCE per node, after a successful Dial -func (n *node) aliveLoop() { - defer n.wg.Done() - ctx, cancel := n.stopCh.NewCtx() - defer cancel() - - { - // sanity check - state := n.State() - switch state { - case NodeStateAlive: - case NodeStateClosed: - return - default: - panic(fmt.Sprintf("aliveLoop can only run for node in Alive state, got: %s", state)) - } - } - - noNewHeadsTimeoutThreshold := n.noNewHeadsThreshold - pollFailureThreshold := n.nodePoolCfg.PollFailureThreshold() - pollInterval := n.nodePoolCfg.PollInterval() - - lggr := logger.Sugared(n.lfcLog).Named("Alive").With("noNewHeadsTimeoutThreshold", noNewHeadsTimeoutThreshold, "pollInterval", pollInterval, "pollFailureThreshold", pollFailureThreshold) - lggr.Tracew("Alive loop starting", "nodeState", n.State()) - - headsC := make(chan *evmtypes.Head) - sub, err := n.EthSubscribe(ctx, headsC, "newHeads") - if err != nil { - lggr.Errorw("Initial subscribe for heads failed", "nodeState", n.State()) - n.declareUnreachable() - return - } - n.aliveLoopSub = sub - defer sub.Unsubscribe() - - var outOfSyncT *time.Ticker - var outOfSyncTC <-chan time.Time - if noNewHeadsTimeoutThreshold > 0 { - lggr.Debugw("Head liveness checking enabled", "nodeState", n.State()) - outOfSyncT = time.NewTicker(noNewHeadsTimeoutThreshold) - defer outOfSyncT.Stop() - outOfSyncTC = outOfSyncT.C - } else { - lggr.Debug("Head liveness checking disabled") - } - - var pollCh <-chan time.Time - if pollInterval > 0 { - lggr.Debug("Polling enabled") - pollT := time.NewTicker(pollInterval) - defer pollT.Stop() - pollCh = pollT.C - if pollFailureThreshold > 0 { - // polling can be enabled with no threshold to enable polling but - // the node will not be marked offline regardless of the number of - // poll failures - lggr.Debug("Polling liveness checking enabled") - } - } else { - lggr.Debug("Polling disabled") - } - - _, highestReceivedBlockNumber, _ := n.StateAndLatest() - var pollFailures uint32 - - for { - select { - case <-ctx.Done(): - return - case <-pollCh: - promEVMPoolRPCNodePolls.WithLabelValues(n.chainID.String(), n.name).Inc() - lggr.Tracew("Polling for version", "nodeState", n.State(), "pollFailures", pollFailures) - var version string - if err := func(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, pollInterval) - defer cancel() - ctx, cancel2 := n.makeQueryCtx(ctx) - defer cancel2() - return n.CallContext(ctx, &version, "web3_clientVersion") - }(ctx); err != nil { - // prevent overflow - if pollFailures < math.MaxUint32 { - promEVMPoolRPCNodePollsFailed.WithLabelValues(n.chainID.String(), n.name).Inc() - pollFailures++ - } - lggr.Warnw(fmt.Sprintf("Poll failure, RPC endpoint %s failed to respond properly", n.String()), "err", err, "pollFailures", pollFailures, "nodeState", n.State()) - } else { - lggr.Debugw("Version poll successful", "nodeState", n.State(), "clientVersion", version) - promEVMPoolRPCNodePollsSuccess.WithLabelValues(n.chainID.String(), n.name).Inc() - pollFailures = 0 - } - if pollFailureThreshold > 0 && pollFailures >= pollFailureThreshold { - lggr.Errorw(fmt.Sprintf("RPC endpoint failed to respond to %d consecutive polls", pollFailures), "pollFailures", pollFailures, "nodeState", n.State()) - if n.nLiveNodes != nil { - if l, _, _ := n.nLiveNodes(); l < 2 { - lggr.Criticalf("RPC endpoint failed to respond to polls; %s %s", msgCannotDisable, msgDegradedState) - continue - } - } - n.declareUnreachable() - return - } - _, num, td := n.StateAndLatest() - if outOfSync, liveNodes := n.syncStatus(num, td); outOfSync { - // note: there must be another live node for us to be out of sync - lggr.Errorw("RPC endpoint has fallen behind", "blockNumber", num, "totalDifficulty", td, "nodeState", n.State()) - if liveNodes < 2 { - lggr.Criticalf("RPC endpoint has fallen behind; %s %s", msgCannotDisable, msgDegradedState) - continue - } - n.declareOutOfSync(n.isOutOfSync) - return - } - case bh, open := <-headsC: - if !open { - lggr.Errorw("Subscription channel unexpectedly closed", "nodeState", n.State()) - n.declareUnreachable() - return - } - promEVMPoolRPCNodeNumSeenBlocks.WithLabelValues(n.chainID.String(), n.name).Inc() - lggr.Tracew("Got head", "head", bh) - if bh.Number > highestReceivedBlockNumber { - promEVMPoolRPCNodeHighestSeenBlock.WithLabelValues(n.chainID.String(), n.name).Set(float64(bh.Number)) - lggr.Tracew("Got higher block number, resetting timer", "latestReceivedBlockNumber", highestReceivedBlockNumber, "blockNumber", bh.Number, "nodeState", n.State()) - highestReceivedBlockNumber = bh.Number - } else { - lggr.Tracew("Ignoring previously seen block number", "latestReceivedBlockNumber", highestReceivedBlockNumber, "blockNumber", bh.Number, "nodeState", n.State()) - } - if outOfSyncT != nil { - outOfSyncT.Reset(noNewHeadsTimeoutThreshold) - } - n.setLatestReceived(bh.Number, bh.TotalDifficulty) - case err := <-sub.Err(): - lggr.Errorw("Subscription was terminated", "err", err, "nodeState", n.State()) - n.declareUnreachable() - return - case <-outOfSyncTC: - // We haven't received a head on the channel for at least the - // threshold amount of time, mark it broken - lggr.Errorw(fmt.Sprintf("RPC endpoint detected out of sync; no new heads received for %s (last head received was %v)", noNewHeadsTimeoutThreshold, highestReceivedBlockNumber), "nodeState", n.State(), "latestReceivedBlockNumber", highestReceivedBlockNumber, "noNewHeadsTimeoutThreshold", noNewHeadsTimeoutThreshold) - if n.nLiveNodes != nil { - if l, _, _ := n.nLiveNodes(); l < 2 { - lggr.Criticalf("RPC endpoint detected out of sync; %s %s", msgCannotDisable, msgDegradedState) - // We don't necessarily want to wait the full timeout to check again, we should - // check regularly and log noisily in this state - outOfSyncT.Reset(zombieNodeCheckInterval(n.noNewHeadsThreshold)) - continue - } - } - n.declareOutOfSync(func(num int64, td *big.Int) bool { return num < highestReceivedBlockNumber }) - return - } - } -} - -func (n *node) isOutOfSync(num int64, td *big.Int) (outOfSync bool) { - outOfSync, _ = n.syncStatus(num, td) - return -} - -// syncStatus returns outOfSync true if num or td is more than SyncThresold behind the best node. -// Always returns outOfSync false for SyncThreshold 0. -// liveNodes is only included when outOfSync is true. -func (n *node) syncStatus(num int64, td *big.Int) (outOfSync bool, liveNodes int) { - if n.nLiveNodes == nil { - return // skip for tests - } - threshold := n.nodePoolCfg.SyncThreshold() - if threshold == 0 { - return // disabled - } - // Check against best node - ln, highest, greatest := n.nLiveNodes() - mode := n.nodePoolCfg.SelectionMode() - switch mode { - case NodeSelectionMode_HighestHead, NodeSelectionMode_RoundRobin, NodeSelectionMode_PriorityLevel: - return num < highest-int64(threshold), ln - case NodeSelectionMode_TotalDifficulty: - bigThreshold := big.NewInt(int64(threshold)) - return td.Cmp(bigmath.Sub(greatest, bigThreshold)) < 0, ln - default: - panic("unrecognized NodeSelectionMode: " + mode) - } -} - -const ( - msgReceivedBlock = "Received block for RPC node, waiting until back in-sync to mark as live again" - msgInSync = "RPC node back in sync" -) - -// outOfSyncLoop takes an OutOfSync node and waits until isOutOfSync returns false to go back to live status -func (n *node) outOfSyncLoop(isOutOfSync func(num int64, td *big.Int) bool) { - defer n.wg.Done() - ctx, cancel := n.stopCh.NewCtx() - defer cancel() - - { - // sanity check - state := n.State() - switch state { - case NodeStateOutOfSync: - case NodeStateClosed: - return - default: - panic(fmt.Sprintf("outOfSyncLoop can only run for node in OutOfSync state, got: %s", state)) - } - } - - outOfSyncAt := time.Now() - - lggr := logger.Sugared(logger.Named(n.lfcLog, "OutOfSync")) - lggr.Debugw("Trying to revive out-of-sync RPC node", "nodeState", n.State()) - - // Need to redial since out-of-sync nodes are automatically disconnected - if err := n.dial(ctx); err != nil { - lggr.Errorw("Failed to dial out-of-sync RPC node", "nodeState", n.State()) - n.declareUnreachable() - return - } - - // Manually re-verify since out-of-sync nodes are automatically disconnected - if err := n.verify(ctx); err != nil { - lggr.Errorw(fmt.Sprintf("Failed to verify out-of-sync RPC node: %v", err), "err", err) - n.declareInvalidChainID() - return - } - - lggr.Tracew("Successfully subscribed to heads feed on out-of-sync RPC node", "nodeState", n.State()) - - ch := make(chan *evmtypes.Head) - subCtx, cancel := n.makeQueryCtx(ctx) - // raw call here to bypass node state checking - sub, err := n.ws.rpc.EthSubscribe(subCtx, ch, "newHeads") - cancel() - if err != nil { - lggr.Errorw("Failed to subscribe heads on out-of-sync RPC node", "nodeState", n.State(), "err", err) - n.declareUnreachable() - return - } - defer sub.Unsubscribe() - - for { - select { - case <-ctx.Done(): - return - case head, open := <-ch: - if !open { - lggr.Error("Subscription channel unexpectedly closed", "nodeState", n.State()) - n.declareUnreachable() - return - } - n.setLatestReceived(head.Number, head.TotalDifficulty) - if !isOutOfSync(head.Number, head.TotalDifficulty) { - // back in-sync! flip back into alive loop - lggr.Infow(fmt.Sprintf("%s: %s. Node was out-of-sync for %s", msgInSync, n.String(), time.Since(outOfSyncAt)), "blockNumber", head.Number, "totalDifficulty", head.TotalDifficulty, "nodeState", n.State()) - n.declareInSync() - return - } - lggr.Debugw(msgReceivedBlock, "blockNumber", head.Number, "totalDifficulty", head.TotalDifficulty, "nodeState", n.State()) - case <-time.After(zombieNodeCheckInterval(n.noNewHeadsThreshold)): - if n.nLiveNodes != nil { - if l, _, _ := n.nLiveNodes(); l < 1 { - lggr.Critical("RPC endpoint is still out of sync, but there are no other available nodes. This RPC node will be forcibly moved back into the live pool in a degraded state") - n.declareInSync() - return - } - } - case err := <-sub.Err(): - lggr.Errorw("Subscription was terminated", "nodeState", n.State(), "err", err) - n.declareUnreachable() - return - } - } -} - -func (n *node) unreachableLoop() { - defer n.wg.Done() - ctx, cancel := n.stopCh.NewCtx() - defer cancel() - - { - // sanity check - state := n.State() - switch state { - case NodeStateUnreachable: - case NodeStateClosed: - return - default: - panic(fmt.Sprintf("unreachableLoop can only run for node in Unreachable state, got: %s", state)) - } - } - - unreachableAt := time.Now() - - lggr := logger.Sugared(logger.Named(n.lfcLog, "Unreachable")) - lggr.Debugw("Trying to revive unreachable RPC node", "nodeState", n.State()) - - dialRetryBackoff := utils.NewRedialBackoff() - - for { - select { - case <-ctx.Done(): - return - case <-time.After(dialRetryBackoff.Duration()): - lggr.Tracew("Trying to re-dial RPC node", "nodeState", n.State()) - - err := n.dial(ctx) - if err != nil { - lggr.Errorw(fmt.Sprintf("Failed to redial RPC node; still unreachable: %v", err), "err", err, "nodeState", n.State()) - continue - } - - n.setState(NodeStateDialed) - - err = n.verify(ctx) - - if pkgerrors.Is(err, errInvalidChainID) { - lggr.Errorw("Failed to redial RPC node; remote endpoint returned the wrong chain ID", "err", err) - n.declareInvalidChainID() - return - } else if err != nil { - lggr.Errorw(fmt.Sprintf("Failed to redial RPC node; verify failed: %v", err), "err", err) - n.declareUnreachable() - return - } - - lggr.Infow(fmt.Sprintf("Successfully redialled and verified RPC node %s. Node was offline for %s", n.String(), time.Since(unreachableAt)), "nodeState", n.State()) - n.declareAlive() - return - } - } -} - -func (n *node) invalidChainIDLoop() { - defer n.wg.Done() - ctx, cancel := n.stopCh.NewCtx() - defer cancel() - - { - // sanity check - state := n.State() - switch state { - case NodeStateInvalidChainID: - case NodeStateClosed: - return - default: - panic(fmt.Sprintf("invalidChainIDLoop can only run for node in InvalidChainID state, got: %s", state)) - } - } - - invalidAt := time.Now() - - lggr := logger.Named(n.lfcLog, "InvalidChainID") - lggr.Debugw(fmt.Sprintf("Periodically re-checking RPC node %s with invalid chain ID", n.String()), "nodeState", n.State()) - - chainIDRecheckBackoff := utils.NewRedialBackoff() - - for { - select { - case <-ctx.Done(): - return - case <-time.After(chainIDRecheckBackoff.Duration()): - err := n.verify(ctx) - if pkgerrors.Is(err, errInvalidChainID) { - lggr.Errorw("Failed to verify RPC node; remote endpoint returned the wrong chain ID", "err", err) - continue - } else if err != nil { - lggr.Errorw(fmt.Sprintf("Unexpected error while verifying RPC node chain ID; %v", err), "err", err) - n.declareUnreachable() - return - } - lggr.Infow(fmt.Sprintf("Successfully verified RPC node. Node was offline for %s", time.Since(invalidAt)), "nodeState", n.State()) - n.declareAlive() - return - } - } -} diff --git a/core/chains/evm/client/node_lifecycle_test.go b/core/chains/evm/client/node_lifecycle_test.go deleted file mode 100644 index 878ecabe600..00000000000 --- a/core/chains/evm/client/node_lifecycle_test.go +++ /dev/null @@ -1,857 +0,0 @@ -package client - -import ( - "fmt" - "math/big" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" - "go.uber.org/zap" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -func standardHandler(method string, _ gjson.Result) (resp testutils.JSONRPCResponse) { - if method == "eth_subscribe" { - resp.Result = `"0x00"` - resp.Notify = HeadResult - return - } - return -} - -func newTestNode(t *testing.T, cfg config.NodePool, noNewHeadsThresholds time.Duration) *node { - return newTestNodeWithCallback(t, cfg, noNewHeadsThresholds, standardHandler) -} - -func newTestNodeWithCallback(t *testing.T, cfg config.NodePool, noNewHeadsThreshold time.Duration, callback testutils.JSONRPCHandler) *node { - s := testutils.NewWSServer(t, testutils.FixtureChainID, callback) - iN := NewNode(cfg, noNewHeadsThreshold, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - return n -} - -// dial sets up the node and puts it into the live state, bypassing the -// normal Start() method which would fire off unwanted goroutines -func dial(t *testing.T, n *node) { - ctx := testutils.Context(t) - require.NoError(t, n.dial(ctx)) - n.setState(NodeStateAlive) - start(t, n) -} - -func start(t *testing.T, n *node) { - // must start to allow closing - err := n.StartOnce("test node", func() error { return nil }) - assert.NoError(t, err) -} - -func makeHeadResult(n int) string { - return fmt.Sprintf( - `{"difficulty":"0xf3a00","extraData":"0xd883010503846765746887676f312e372e318664617277696e","gasLimit":"0xffc001","gasUsed":"0x0","hash":"0x41800b5c3f1717687d85fc9018faac0a6e90b39deaa0b99e7fe4fe796ddeb26a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xd1aeb42885a43b72b518182ef893125814811048","mixHash":"0x0f98b15f1a4901a7e9204f3c500a7bd527b3fb2c3340e12176a44b83e414a69e","nonce":"0x0ece08ea8c49dfd9","number":"%s","parentHash":"0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x218","stateRoot":"0xc7b01007a10da045eacb90385887dd0c38fcb5db7393006bdde24b93873c334b","timestamp":"0x58318da2","totalDifficulty":"0x1f3a00","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`, - testutils.IntToHex(n), - ) -} - -func makeNewHeadWSMessage(n int) string { - return fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x00","result":%s}}`, makeHeadResult(n)) -} - -func TestUnit_NodeLifecycle_aliveLoop(t *testing.T) { - t.Parallel() - - t.Run("with no poll and sync timeouts, exits on close", func(t *testing.T) { - pollAndSyncTimeoutsDisabledCfg := TestNodePoolConfig{} - n := newTestNode(t, pollAndSyncTimeoutsDisabledCfg, 0*time.Second) - dial(t, n) - - ch := make(chan struct{}) - n.wg.Add(1) - go func() { - defer close(ch) - n.aliveLoop() - }() - assert.NoError(t, n.Close()) - testutils.WaitWithTimeout(t, ch, "expected aliveLoop to exit") - }) - - t.Run("with no poll failures past threshold, stays alive", func(t *testing.T) { - threshold := 5 - cfg := TestNodePoolConfig{NodePollFailureThreshold: uint32(threshold), NodePollInterval: testutils.TestInterval} - var calls atomic.Int32 - n := newTestNodeWithCallback(t, cfg, time.Second*0, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - defer calls.Add(1) - // It starts working right before it hits threshold - if int(calls.Load())+1 >= threshold { - resp.Result = `"test client version"` - return - } - resp.Result = "this will error" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.AssertEventually(t, func() bool { - // Need to wait for one complete cycle before checking state so add - // 1 to threshold - return int(calls.Load()) > threshold+1 - }) - - assert.Equal(t, NodeStateAlive, n.State()) - }) - - t.Run("with threshold poll failures, transitions to unreachable", func(t *testing.T) { - syncTimeoutsDisabledCfg := TestNodePoolConfig{NodePollFailureThreshold: 3, NodePollInterval: testutils.TestInterval} - n := newTestNode(t, syncTimeoutsDisabledCfg, time.Second*0) - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateUnreachable - }) - }) - - t.Run("with threshold poll failures, but we are the last node alive, forcibly keeps it alive", func(t *testing.T) { - threshold := 3 - cfg := TestNodePoolConfig{NodePollFailureThreshold: uint32(threshold), NodePollInterval: testutils.TestInterval} - var calls atomic.Int32 - n := newTestNodeWithCallback(t, cfg, time.Second*0, func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = HeadResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - defer calls.Add(1) - resp.Error.Message = "this will error" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - n.nLiveNodes = func() (int, int64, *big.Int) { return 1, 0, nil } - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.AssertEventually(t, func() bool { - // Need to wait for one complete cycle before checking state so add - // 1 to threshold - return int(calls.Load()) > threshold+1 - }) - - assert.Equal(t, NodeStateAlive, n.State()) - }) - - t.Run("if initial subscribe fails, transitions to unreachable", func(t *testing.T) { - pollDisabledCfg := TestNodePoolConfig{} - n := newTestNodeWithCallback(t, pollDisabledCfg, testutils.TestInterval, func(string, gjson.Result) (resp testutils.JSONRPCResponse) { return }) - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - _, err := n.EthSubscribe(testutils.Context(t), make(chan *evmtypes.Head)) - assert.Error(t, err) - - n.wg.Add(1) - n.aliveLoop() - - assert.Equal(t, NodeStateUnreachable, n.State()) - // sc-39341: ensure failed EthSubscribe didn't register a (*rpc.ClientSubscription)(nil) which would lead to a panic on Unsubscribe - assert.Len(t, n.subs, 0) - }) - - t.Run("if remote RPC connection is closed transitions to unreachable", func(t *testing.T) { - // NoNewHeadsThreshold needs to be positive but must be very large so - // we don't time out waiting for a new head before we have a chance to - // handle the server disconnect - cfg := TestNodePoolConfig{NodePollInterval: 1 * time.Second} - chSubbed := make(chan struct{}, 1) - chPolled := make(chan struct{}) - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - case "web3_clientVersion": - select { - case chPolled <- struct{}{}: - default: - } - resp.Result = `"test client version 2"` - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, testutils.WaitTimeout(t), logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription") - testutils.WaitWithTimeout(t, chPolled, "timed out waiting for initial poll") - - assert.Equal(t, NodeStateAlive, n.State()) - - // Simulate remote websocket disconnect - // This causes sub.Err() to close - s.Close() - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateUnreachable - }) - }) - - t.Run("when no new heads received for threshold, transitions to out of sync", func(t *testing.T) { - cfg := TestNodePoolConfig{} - chSubbed := make(chan struct{}, 2) - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - resp.Result = `"test client version 2"` - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, 1*time.Second, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for InSync") - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateOutOfSync - }) - - // Otherwise, there may be data race on dial() vs Close() (accessing ws.rpc) - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for OutOfSync") - }) - - t.Run("when no new heads received for threshold but we are the last live node, forcibly stays alive", func(t *testing.T) { - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - pollDisabledCfg := TestNodePoolConfig{} - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - case "eth_unsubscribe": - resp.Result = "true" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(pollDisabledCfg, testutils.TestInterval, lggr, *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (int, int64, *big.Int) { return 1, 0, nil } - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - // to avoid timing-dependent tests, simply wait for the log message instead - // wait for the log twice to be sure we have fully completed the code path and gone around the loop - testutils.WaitForLogMessageCount(t, observedLogs, msgCannotDisable, 2) - - assert.Equal(t, NodeStateAlive, n.State()) - }) - - t.Run("when behind more than SyncThreshold, transitions to out of sync", func(t *testing.T) { - cfg := TestNodePoolConfig{NodeSyncThreshold: 10, NodePollFailureThreshold: 2, NodePollInterval: 100 * time.Millisecond, NodeSelectionMode: NodeSelectionMode_HighestHead} - chSubbed := make(chan struct{}, 2) - var highestHead atomic.Int64 - const stall = 10 - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(int(highestHead.Load())) - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - resp.Result = `"test client version 2"` - // always tick each poll, but only signal back up to stall - if n := highestHead.Add(1); n <= stall { - resp.Notify = makeHeadResult(int(n)) - } - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, 0*time.Second, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (count int, blockNumber int64, totalDifficulty *big.Int) { - return 2, highestHead.Load(), nil - } - - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for InSync") - - // ensure alive up to stall - testutils.AssertEventually(t, func() bool { - state, num, _ := n.StateAndLatest() - if num < stall { - require.Equal(t, NodeStateAlive, state) - } - return num == stall - }) - - testutils.AssertEventually(t, func() bool { - state, num, _ := n.StateAndLatest() - return state == NodeStateOutOfSync && num == stall - }) - assert.GreaterOrEqual(t, highestHead.Load(), int64(stall+cfg.SyncThreshold())) - - // Otherwise, there may be data race on dial() vs Close() (accessing ws.rpc) - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for OutOfSync") - }) - - t.Run("when behind but SyncThreshold=0, stay alive", func(t *testing.T) { - cfg := TestNodePoolConfig{NodeSyncThreshold: 0, NodePollFailureThreshold: 2, NodePollInterval: 100 * time.Millisecond, NodeSelectionMode: NodeSelectionMode_HighestHead} - chSubbed := make(chan struct{}, 1) - var highestHead atomic.Int64 - const stall = 10 - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(int(highestHead.Load())) - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - resp.Result = `"test client version 2"` - // always tick each poll, but only signal back up to stall - if n := highestHead.Add(1); n <= stall { - resp.Notify = makeHeadResult(int(n)) - } - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, 0*time.Second, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (count int, blockNumber int64, totalDifficulty *big.Int) { - return 2, highestHead.Load(), nil - } - - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for InSync") - - // ensure alive up to stall - testutils.AssertEventually(t, func() bool { - state, num, _ := n.StateAndLatest() - require.Equal(t, NodeStateAlive, state) - return num == stall - }) - - assert.Equal(t, NodeStateAlive, n.state) - assert.GreaterOrEqual(t, highestHead.Load(), int64(stall+cfg.SyncThreshold())) - }) - - t.Run("when behind more than SyncThreshold but we are the last live node, forcibly stays alive", func(t *testing.T) { - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - cfg := TestNodePoolConfig{NodeSyncThreshold: 5, NodePollFailureThreshold: 2, NodePollInterval: 100 * time.Millisecond, NodeSelectionMode: NodeSelectionMode_HighestHead} - chSubbed := make(chan struct{}, 1) - var highestHead atomic.Int64 - const stall = 10 - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - n := highestHead.Load() - if n > stall { - n = stall - } - resp.Notify = makeHeadResult(int(n)) - return - case "eth_unsubscribe": - resp.Result = "true" - return - case "web3_clientVersion": - resp.Result = `"test client version 2"` - // always tick each poll, but only signal back up to stall - if n := highestHead.Add(1); n <= stall { - resp.Notify = makeHeadResult(int(n)) - } - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, 0*time.Second, lggr, *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (count int, blockNumber int64, totalDifficulty *big.Int) { - return 1, highestHead.Load(), nil - } - - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.aliveLoop() - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription for InSync") - - // ensure alive up to stall - testutils.AssertEventually(t, func() bool { - state, num, _ := n.StateAndLatest() - require.Equal(t, NodeStateAlive, state) - return num == stall - }) - - assert.Equal(t, NodeStateAlive, n.state) - testutils.AssertEventually(t, func() bool { - return highestHead.Load() >= int64(stall+cfg.SyncThreshold()) - }) - - testutils.WaitForLogMessageCount(t, observedLogs, msgCannotDisable, 1) - - state, num, _ := n.StateAndLatest() - assert.Equal(t, NodeStateAlive, state) - assert.Equal(t, int64(stall), num) - }) -} - -func TestUnit_NodeLifecycle_outOfSyncLoop(t *testing.T) { - t.Parallel() - - t.Run("exits on close", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNode(t, cfg, time.Second*0) - dial(t, n) - n.setState(NodeStateOutOfSync) - - ch := make(chan struct{}) - - n.wg.Add(1) - go func() { - defer close(ch) - n.outOfSyncLoop(func(num int64, td *big.Int) bool { return false }) - }() - assert.NoError(t, n.Close()) - testutils.WaitWithTimeout(t, ch, "expected outOfSyncLoop to exit") - }) - - t.Run("if initial subscribe fails, transitions to unreachable", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNodeWithCallback(t, cfg, time.Second*0, func(string, gjson.Result) (resp testutils.JSONRPCResponse) { return }) - dial(t, n) - n.setState(NodeStateOutOfSync) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - - n.outOfSyncLoop(func(num int64, td *big.Int) bool { return num == 0 }) - assert.Equal(t, NodeStateUnreachable, n.State()) - }) - - t.Run("transitions to unreachable if remote RPC subscription channel closed", func(t *testing.T) { - cfg := TestNodePoolConfig{} - chSubbed := make(chan struct{}, 1) - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, time.Second, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - - dial(t, n) - n.setState(NodeStateOutOfSync) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.outOfSyncLoop(func(num int64, td *big.Int) bool { return num == 0 }) - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription") - - assert.Equal(t, NodeStateOutOfSync, n.State()) - - // Simulate remote websocket disconnect - // This causes sub.Err() to close - s.Close() - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateUnreachable - }) - }) - - t.Run("transitions to alive if it receives a newer head", func(t *testing.T) { - // NoNewHeadsThreshold needs to be positive but must be very large so - // we don't time out waiting for a new head before we have a chance to - // handle the server disconnect - lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - cfg := TestNodePoolConfig{} - chSubbed := make(chan struct{}, 1) - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeNewHeadWSMessage(42) - return - case "eth_unsubscribe": - resp.Result = "true" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, time.Second*0, lggr, *s.WSURL(), nil, "test node", 0, testutils.FixtureChainID, 1) - n := iN.(*node) - - start(t, n) - n.setState(NodeStateOutOfSync) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.outOfSyncLoop(func(num int64, td *big.Int) bool { return num < 43 }) - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription") - - assert.Equal(t, NodeStateOutOfSync, n.State()) - - // heads less than latest seen head are ignored; they do not make the node live - for i := 0; i < 43; i++ { - msg := makeNewHeadWSMessage(i) - s.MustWriteBinaryMessageSync(t, msg) - testutils.WaitForLogMessageCount(t, observedLogs, msgReceivedBlock, i+1) - assert.Equal(t, NodeStateOutOfSync, n.State()) - } - - msg := makeNewHeadWSMessage(43) - s.MustWriteBinaryMessageSync(t, msg) - - testutils.AssertEventually(t, func() bool { - s, n, td := n.StateAndLatest() - return s == NodeStateAlive && n != -1 && td != nil - }) - - testutils.WaitForLogMessage(t, observedLogs, msgInSync) - }) - - t.Run("transitions to alive if back in-sync", func(t *testing.T) { - lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - cfg := TestNodePoolConfig{NodeSyncThreshold: 5, NodeSelectionMode: NodeSelectionMode_HighestHead} - chSubbed := make(chan struct{}, 1) - const stall = 42 - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeNewHeadWSMessage(stall) - return - case "eth_unsubscribe": - resp.Result = "true" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, time.Second*0, lggr, *s.WSURL(), nil, "test node", 0, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (count int, blockNumber int64, totalDifficulty *big.Int) { - return 2, stall + int64(cfg.SyncThreshold()), nil - } - - start(t, n) - n.setState(NodeStateOutOfSync) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.outOfSyncLoop(n.isOutOfSync) - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription") - - assert.Equal(t, NodeStateOutOfSync, n.State()) - - // heads less than stall (latest seen head - SyncThreshold) are ignored; they do not make the node live - for i := 0; i < stall; i++ { - msg := makeNewHeadWSMessage(i) - s.MustWriteBinaryMessageSync(t, msg) - testutils.WaitForLogMessageCount(t, observedLogs, msgReceivedBlock, i+1) - assert.Equal(t, NodeStateOutOfSync, n.State()) - } - - msg := makeNewHeadWSMessage(stall) - s.MustWriteBinaryMessageSync(t, msg) - - testutils.AssertEventually(t, func() bool { - s, n, td := n.StateAndLatest() - return s == NodeStateAlive && n != -1 && td != nil - }) - - testutils.WaitForLogMessage(t, observedLogs, msgInSync) - }) - - t.Run("if no live nodes are available, forcibly marks this one alive again", func(t *testing.T) { - cfg := TestNodePoolConfig{} - chSubbed := make(chan struct{}, 1) - s := testutils.NewWSServer(t, testutils.FixtureChainID, - func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - chSubbed <- struct{}{} - resp.Result = `"0x00"` - resp.Notify = makeHeadResult(0) - return - case "eth_unsubscribe": - resp.Result = "true" - return - default: - t.Errorf("unexpected RPC method: %s", method) - } - return - }) - - iN := NewNode(cfg, testutils.TestInterval, logger.Test(t), *s.WSURL(), nil, "test node", 42, testutils.FixtureChainID, 1) - n := iN.(*node) - n.nLiveNodes = func() (int, int64, *big.Int) { return 0, 0, nil } - - dial(t, n) - n.setState(NodeStateOutOfSync) - defer func() { assert.NoError(t, n.Close()) }() - - n.wg.Add(1) - go n.outOfSyncLoop(func(num int64, td *big.Int) bool { return num == 0 }) - - testutils.WaitWithTimeout(t, chSubbed, "timed out waiting for initial subscription") - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateAlive - }) - }) -} - -func TestUnit_NodeLifecycle_unreachableLoop(t *testing.T) { - t.Parallel() - - t.Run("exits on close", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNode(t, cfg, time.Second*0) - start(t, n) - n.setState(NodeStateUnreachable) - - ch := make(chan struct{}) - n.wg.Add(1) - go func() { - n.unreachableLoop() - close(ch) - }() - assert.NoError(t, n.Close()) - testutils.WaitWithTimeout(t, ch, "expected unreachableLoop to exit") - }) - - t.Run("on successful redial and verify, transitions to alive", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNode(t, cfg, time.Second*0) - start(t, n) - defer func() { assert.NoError(t, n.Close()) }() - n.setState(NodeStateUnreachable) - n.wg.Add(1) - - go n.unreachableLoop() - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateAlive - }) - }) - - t.Run("on successful redial but failed verify, transitions to invalid chain ID", func(t *testing.T) { - cfg := TestNodePoolConfig{} - s := testutils.NewWSServer(t, testutils.FixtureChainID, standardHandler) - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - iN := NewNode(cfg, time.Second*0, lggr, *s.WSURL(), nil, "test node", 0, big.NewInt(42), 1) - n := iN.(*node) - defer func() { assert.NoError(t, n.Close()) }() - start(t, n) - n.setState(NodeStateUnreachable) - n.wg.Add(1) - - go n.unreachableLoop() - - testutils.WaitForLogMessage(t, observedLogs, "Failed to redial RPC node; remote endpoint returned the wrong chain ID") - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateInvalidChainID - }) - }) - - t.Run("on failed redial, keeps trying to redial", func(t *testing.T) { - cfg := TestNodePoolConfig{} - lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - iN := NewNode(cfg, time.Second*0, lggr, *testutils.MustParseURL(t, "ws://test.invalid"), nil, "test node", 0, big.NewInt(42), 1) - n := iN.(*node) - defer func() { assert.NoError(t, n.Close()) }() - start(t, n) - n.setState(NodeStateUnreachable) - n.wg.Add(1) - - go n.unreachableLoop() - - testutils.WaitForLogMessageCount(t, observedLogs, "Failed to redial RPC node", 3) - - assert.Equal(t, NodeStateUnreachable, n.State()) - }) -} -func TestUnit_NodeLifecycle_invalidChainIDLoop(t *testing.T) { - t.Parallel() - - t.Run("exits on close", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNode(t, cfg, time.Second*0) - start(t, n) - n.setState(NodeStateInvalidChainID) - - ch := make(chan struct{}) - n.wg.Add(1) - go func() { - n.invalidChainIDLoop() - close(ch) - }() - assert.NoError(t, n.Close()) - testutils.WaitWithTimeout(t, ch, "expected invalidChainIDLoop to exit") - }) - - t.Run("on successful verify, transitions to alive", func(t *testing.T) { - cfg := TestNodePoolConfig{} - n := newTestNode(t, cfg, time.Second*0) - dial(t, n) - defer func() { assert.NoError(t, n.Close()) }() - n.setState(NodeStateInvalidChainID) - n.wg.Add(1) - - go n.invalidChainIDLoop() - - testutils.AssertEventually(t, func() bool { - return n.State() == NodeStateAlive - }) - }) - - t.Run("on failed verify, keeps checking", func(t *testing.T) { - cfg := TestNodePoolConfig{} - s := testutils.NewWSServer(t, testutils.FixtureChainID, standardHandler) - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - iN := NewNode(cfg, time.Second*0, lggr, *s.WSURL(), nil, "test node", 0, big.NewInt(42), 1) - n := iN.(*node) - defer func() { assert.NoError(t, n.Close()) }() - dial(t, n) - n.setState(NodeStateUnreachable) - n.wg.Add(1) - - go n.unreachableLoop() - - testutils.WaitForLogMessageCount(t, observedLogs, "Failed to redial RPC node; remote endpoint returned the wrong chain ID", 3) - - assert.Equal(t, NodeStateInvalidChainID, n.State()) - }) -} diff --git a/core/chains/evm/client/node_selector_highest_head.go b/core/chains/evm/client/node_selector_highest_head.go deleted file mode 100644 index 2ed41486cff..00000000000 --- a/core/chains/evm/client/node_selector_highest_head.go +++ /dev/null @@ -1,32 +0,0 @@ -package client - -import ( - "math" -) - -type highestHeadNodeSelector []Node - -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewHighestHeadNodeSelector] -func NewHighestHeadNodeSelector(nodes []Node) NodeSelector { - return highestHeadNodeSelector(nodes) -} - -func (s highestHeadNodeSelector) Select() Node { - var highestHeadNumber int64 = math.MinInt64 - var highestHeadNodes []Node - for _, n := range s { - state, currentHeadNumber, _ := n.StateAndLatest() - if state == NodeStateAlive && currentHeadNumber >= highestHeadNumber { - if highestHeadNumber < currentHeadNumber { - highestHeadNumber = currentHeadNumber - highestHeadNodes = nil - } - highestHeadNodes = append(highestHeadNodes, n) - } - } - return firstOrHighestPriority(highestHeadNodes) -} - -func (s highestHeadNodeSelector) Name() string { - return NodeSelectionMode_HighestHead -} diff --git a/core/chains/evm/client/node_selector_highest_head_test.go b/core/chains/evm/client/node_selector_highest_head_test.go deleted file mode 100644 index 29b39b7fe73..00000000000 --- a/core/chains/evm/client/node_selector_highest_head_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package client_test - -import ( - "testing" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" - - "github.com/stretchr/testify/assert" -) - -func TestHighestHeadNodeSelectorName(t *testing.T) { - selector := evmclient.NewHighestHeadNodeSelector(nil) - assert.Equal(t, selector.Name(), evmclient.NodeSelectionMode_HighestHead) -} - -func TestHighestHeadNodeSelector(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("StateAndLatest").Return(evmclient.NodeStateOutOfSync, int64(-1), nil) - } else if i == 1 { - // second node is alive, LatestReceivedBlockNumber = 1 - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), nil) - } else { - // third node is alive, LatestReceivedBlockNumber = 2 (best node) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(2), nil) - } - node.On("Order").Maybe().Return(int32(1)) - nodes = append(nodes, node) - } - - selector := evmclient.NewHighestHeadNodeSelector(nodes) - assert.Same(t, nodes[2], selector.Select()) - - t.Run("stick to the same node", func(t *testing.T) { - node := evmmocks.NewNode(t) - // fourth node is alive, LatestReceivedBlockNumber = 2 (same as 3rd) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(2), nil) - node.On("Order").Return(int32(1)) - nodes = append(nodes, node) - - selector := evmclient.NewHighestHeadNodeSelector(nodes) - assert.Same(t, nodes[2], selector.Select()) - }) - - t.Run("another best node", func(t *testing.T) { - node := evmmocks.NewNode(t) - // fifth node is alive, LatestReceivedBlockNumber = 3 (better than 3rd and 4th) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), nil) - node.On("Order").Return(int32(1)) - nodes = append(nodes, node) - - selector := evmclient.NewHighestHeadNodeSelector(nodes) - assert.Same(t, nodes[4], selector.Select()) - }) - - t.Run("nodes never update latest block number", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(-1), nil) - node1.On("Order").Return(int32(1)) - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(-1), nil) - node2.On("Order").Return(int32(1)) - nodes := []evmclient.Node{node1, node2} - - selector := evmclient.NewHighestHeadNodeSelector(nodes) - assert.Same(t, node1, selector.Select()) - }) -} - -func TestHighestHeadNodeSelector_None(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("StateAndLatest").Return(evmclient.NodeStateOutOfSync, int64(-1), nil) - } else { - // others are unreachable - node.On("StateAndLatest").Return(evmclient.NodeStateUnreachable, int64(1), nil) - } - nodes = append(nodes, node) - } - - selector := evmclient.NewHighestHeadNodeSelector(nodes) - assert.Nil(t, selector.Select()) -} - -func TestHighestHeadNodeSelectorWithOrder(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - t.Run("same head and order", func(t *testing.T) { - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), nil) - node.On("Order").Return(int32(2)) - nodes = append(nodes, node) - } - selector := evmclient.NewHighestHeadNodeSelector(nodes) - //Should select the first node because all things are equal - assert.Same(t, nodes[0], selector.Select()) - }) - - t.Run("same head but different order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), nil) - node1.On("Order").Return(int32(3)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), nil) - node2.On("Order").Return(int32(1)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), nil) - node3.On("Order").Return(int32(2)) - - nodes := []evmclient.Node{node1, node2, node3} - selector := evmclient.NewHighestHeadNodeSelector(nodes) - //Should select the second node as it has the highest priority - assert.Same(t, nodes[1], selector.Select()) - }) - - t.Run("different head but same order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), nil) - node1.On("Order").Maybe().Return(int32(3)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(2), nil) - node2.On("Order").Maybe().Return(int32(3)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), nil) - node3.On("Order").Return(int32(3)) - - nodes := []evmclient.Node{node1, node2, node3} - selector := evmclient.NewHighestHeadNodeSelector(nodes) - //Should select the third node as it has the highest head - assert.Same(t, nodes[2], selector.Select()) - }) - - t.Run("different head and different order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(10), nil) - node1.On("Order").Maybe().Return(int32(3)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(11), nil) - node2.On("Order").Maybe().Return(int32(4)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(11), nil) - node3.On("Order").Maybe().Return(int32(3)) - - node4 := evmmocks.NewNode(t) - node4.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(10), nil) - node4.On("Order").Maybe().Return(int32(1)) - - nodes := []evmclient.Node{node1, node2, node3, node4} - selector := evmclient.NewHighestHeadNodeSelector(nodes) - //Should select the third node as it has the highest head and will win the priority tie-breaker - assert.Same(t, nodes[2], selector.Select()) - }) -} diff --git a/core/chains/evm/client/node_selector_priority_level.go b/core/chains/evm/client/node_selector_priority_level.go deleted file mode 100644 index fba6d403327..00000000000 --- a/core/chains/evm/client/node_selector_priority_level.go +++ /dev/null @@ -1,104 +0,0 @@ -package client - -import ( - "math" - "sort" - "sync/atomic" -) - -type priorityLevelNodeSelector struct { - nodes []Node - roundRobinCount []atomic.Uint32 -} - -type nodeWithPriority struct { - node Node - priority int32 -} - -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewPriorityLevelNodeSelector] -func NewPriorityLevelNodeSelector(nodes []Node) NodeSelector { - return &priorityLevelNodeSelector{ - nodes: nodes, - roundRobinCount: make([]atomic.Uint32, nrOfPriorityTiers(nodes)), - } -} - -func (s priorityLevelNodeSelector) Select() Node { - nodes := s.getHighestPriorityAliveTier() - - if len(nodes) == 0 { - return nil - } - priorityLevel := nodes[len(nodes)-1].priority - - // NOTE: Inc returns the number after addition, so we must -1 to get the "current" counter - count := s.roundRobinCount[priorityLevel].Add(1) - 1 - idx := int(count % uint32(len(nodes))) - - return nodes[idx].node -} - -func (s priorityLevelNodeSelector) Name() string { - return NodeSelectionMode_PriorityLevel -} - -// getHighestPriorityAliveTier filters nodes that are not in state NodeStateAlive and -// returns only the highest tier of alive nodes -func (s priorityLevelNodeSelector) getHighestPriorityAliveTier() []nodeWithPriority { - var nodes []nodeWithPriority - for _, n := range s.nodes { - if n.State() == NodeStateAlive { - nodes = append(nodes, nodeWithPriority{n, n.Order()}) - } - } - - if len(nodes) == 0 { - return nil - } - - return removeLowerTiers(nodes) -} - -// removeLowerTiers take a slice of nodeWithPriority and keeps only the highest tier -func removeLowerTiers(nodes []nodeWithPriority) []nodeWithPriority { - sort.SliceStable(nodes, func(i, j int) bool { - return nodes[i].priority > nodes[j].priority - }) - - var nodes2 []nodeWithPriority - currentPriority := nodes[len(nodes)-1].priority - - for _, n := range nodes { - if n.priority == currentPriority { - nodes2 = append(nodes2, n) - } - } - - return nodes2 -} - -// nrOfPriorityTiers calculates the total number of priority tiers -func nrOfPriorityTiers(nodes []Node) int32 { - highestPriority := int32(0) - for _, n := range nodes { - priority := n.Order() - if highestPriority < priority { - highestPriority = priority - } - } - return highestPriority + 1 -} - -// firstOrHighestPriority takes a list of nodes and returns the first one with the highest priority -func firstOrHighestPriority(nodes []Node) Node { - hp := int32(math.MaxInt32) - var node Node - for _, n := range nodes { - if n.Order() < hp { - hp = n.Order() - node = n - } - } - return node -} diff --git a/core/chains/evm/client/node_selector_priority_level_test.go b/core/chains/evm/client/node_selector_priority_level_test.go deleted file mode 100644 index a7c68c3f282..00000000000 --- a/core/chains/evm/client/node_selector_priority_level_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package client_test - -import ( - "testing" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" - - "github.com/stretchr/testify/assert" -) - -func TestPriorityLevelNodeSelectorName(t *testing.T) { - selector := evmclient.NewPriorityLevelNodeSelector(nil) - assert.Equal(t, selector.Name(), evmclient.NodeSelectionMode_PriorityLevel) -} - -func TestPriorityLevelNodeSelector(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - n1 := evmmocks.NewNode(t) - n1.On("State").Return(evmclient.NodeStateAlive) - n1.On("Order").Return(int32(1)) - - n2 := evmmocks.NewNode(t) - n2.On("State").Return(evmclient.NodeStateAlive) - n2.On("Order").Return(int32(1)) - - n3 := evmmocks.NewNode(t) - n3.On("State").Return(evmclient.NodeStateAlive) - n3.On("Order").Return(int32(1)) - - nodes = append(nodes, n1, n2, n3) - selector := evmclient.NewPriorityLevelNodeSelector(nodes) - assert.Same(t, nodes[0], selector.Select()) - assert.Same(t, nodes[1], selector.Select()) - assert.Same(t, nodes[2], selector.Select()) - assert.Same(t, nodes[0], selector.Select()) - assert.Same(t, nodes[1], selector.Select()) - assert.Same(t, nodes[2], selector.Select()) -} - -func TestPriorityLevelNodeSelector_None(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("State").Return(evmclient.NodeStateOutOfSync) - node.On("Order").Return(int32(1)) - } else { - // others are unreachable - node.On("State").Return(evmclient.NodeStateUnreachable) - node.On("Order").Return(int32(1)) - } - nodes = append(nodes, node) - } - - selector := evmclient.NewPriorityLevelNodeSelector(nodes) - assert.Nil(t, selector.Select()) -} - -func TestPriorityLevelNodeSelector_DifferentOrder(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - n1 := evmmocks.NewNode(t) - n1.On("State").Return(evmclient.NodeStateAlive) - n1.On("Order").Return(int32(1)) - - n2 := evmmocks.NewNode(t) - n2.On("State").Return(evmclient.NodeStateAlive) - n2.On("Order").Return(int32(2)) - - n3 := evmmocks.NewNode(t) - n3.On("State").Return(evmclient.NodeStateAlive) - n3.On("Order").Return(int32(3)) - - nodes = append(nodes, n1, n2, n3) - selector := evmclient.NewPriorityLevelNodeSelector(nodes) - assert.Same(t, nodes[0], selector.Select()) - assert.Same(t, nodes[0], selector.Select()) -} diff --git a/core/chains/evm/client/node_selector_round_robin.go b/core/chains/evm/client/node_selector_round_robin.go deleted file mode 100644 index 3bd19f0ede4..00000000000 --- a/core/chains/evm/client/node_selector_round_robin.go +++ /dev/null @@ -1,39 +0,0 @@ -package client - -import "sync/atomic" - -type roundRobinSelector struct { - nodes []Node - roundRobinCount atomic.Uint32 -} - -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewRoundRobinSelector] -func NewRoundRobinSelector(nodes []Node) NodeSelector { - return &roundRobinSelector{ - nodes: nodes, - } -} - -func (s *roundRobinSelector) Select() Node { - var liveNodes []Node - for _, n := range s.nodes { - if n.State() == NodeStateAlive { - liveNodes = append(liveNodes, n) - } - } - - nNodes := len(liveNodes) - if nNodes == 0 { - return nil - } - - // NOTE: Inc returns the number after addition, so we must -1 to get the "current" counter - count := s.roundRobinCount.Add(1) - 1 - idx := int(count % uint32(nNodes)) - - return liveNodes[idx] -} - -func (s *roundRobinSelector) Name() string { - return NodeSelectionMode_RoundRobin -} diff --git a/core/chains/evm/client/node_selector_round_robin_test.go b/core/chains/evm/client/node_selector_round_robin_test.go deleted file mode 100644 index 8308d779895..00000000000 --- a/core/chains/evm/client/node_selector_round_robin_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package client_test - -import ( - "testing" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" - - "github.com/stretchr/testify/assert" -) - -func TestRoundRobinNodeSelector(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("State").Return(evmclient.NodeStateOutOfSync) - } else { - // second & third nodes are alive - node.On("State").Return(evmclient.NodeStateAlive) - } - nodes = append(nodes, node) - } - - selector := evmclient.NewRoundRobinSelector(nodes) - assert.Same(t, nodes[1], selector.Select()) - assert.Same(t, nodes[2], selector.Select()) - assert.Same(t, nodes[1], selector.Select()) - assert.Same(t, nodes[2], selector.Select()) -} - -func TestRoundRobinNodeSelector_None(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("State").Return(evmclient.NodeStateOutOfSync) - } else { - // others are unreachable - node.On("State").Return(evmclient.NodeStateUnreachable) - } - nodes = append(nodes, node) - } - - selector := evmclient.NewRoundRobinSelector(nodes) - assert.Nil(t, selector.Select()) -} diff --git a/core/chains/evm/client/node_selector_total_difficulty.go b/core/chains/evm/client/node_selector_total_difficulty.go deleted file mode 100644 index 27d888947d9..00000000000 --- a/core/chains/evm/client/node_selector_total_difficulty.go +++ /dev/null @@ -1,43 +0,0 @@ -package client - -import "math/big" - -type totalDifficultyNodeSelector []Node - -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewTotalDifficultyNodeSelector] -func NewTotalDifficultyNodeSelector(nodes []Node) NodeSelector { - return totalDifficultyNodeSelector(nodes) -} - -func (s totalDifficultyNodeSelector) Select() Node { - // NodeNoNewHeadsThreshold may not be enabled, in this case all nodes have td == nil - var highestTD *big.Int - var nodes []Node - var aliveNodes []Node - - for _, n := range s { - state, _, currentTD := n.StateAndLatest() - if state != NodeStateAlive { - continue - } - - aliveNodes = append(aliveNodes, n) - if currentTD != nil && (highestTD == nil || currentTD.Cmp(highestTD) >= 0) { - if highestTD == nil || currentTD.Cmp(highestTD) > 0 { - highestTD = currentTD - nodes = nil - } - nodes = append(nodes, n) - } - } - - //If all nodes have td == nil pick one from the nodes that are alive - if len(nodes) == 0 { - return firstOrHighestPriority(aliveNodes) - } - return firstOrHighestPriority(nodes) -} - -func (s totalDifficultyNodeSelector) Name() string { - return NodeSelectionMode_TotalDifficulty -} diff --git a/core/chains/evm/client/node_selector_total_difficulty_test.go b/core/chains/evm/client/node_selector_total_difficulty_test.go deleted file mode 100644 index 486a421477e..00000000000 --- a/core/chains/evm/client/node_selector_total_difficulty_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package client_test - -import ( - "math/big" - "testing" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" - - "github.com/stretchr/testify/assert" -) - -func TestTotalDifficultyNodeSelectorName(t *testing.T) { - selector := evmclient.NewTotalDifficultyNodeSelector(nil) - assert.Equal(t, selector.Name(), evmclient.NodeSelectionMode_TotalDifficulty) -} - -func TestTotalDifficultyNodeSelector(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("StateAndLatest").Return(evmclient.NodeStateOutOfSync, int64(-1), nil) - } else if i == 1 { - // second node is alive - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(7)) - } else { - // third node is alive and best - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(2), big.NewInt(8)) - } - node.On("Order").Maybe().Return(int32(1)) - nodes = append(nodes, node) - } - - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - assert.Same(t, nodes[2], selector.Select()) - - t.Run("stick to the same node", func(t *testing.T) { - node := evmmocks.NewNode(t) - // fourth node is alive (same as 3rd) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(2), big.NewInt(8)) - node.On("Order").Maybe().Return(int32(1)) - nodes = append(nodes, node) - - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - assert.Same(t, nodes[2], selector.Select()) - }) - - t.Run("another best node", func(t *testing.T) { - node := evmmocks.NewNode(t) - // fifth node is alive (better than 3rd and 4th) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), big.NewInt(11)) - node.On("Order").Maybe().Return(int32(1)) - nodes = append(nodes, node) - - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - assert.Same(t, nodes[4], selector.Select()) - }) - - t.Run("nodes never update latest block number", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(-1), nil) - node1.On("Order").Maybe().Return(int32(1)) - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(-1), nil) - node2.On("Order").Maybe().Return(int32(1)) - nodes := []evmclient.Node{node1, node2} - - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - assert.Same(t, node1, selector.Select()) - }) -} - -func TestTotalDifficultyNodeSelector_None(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - if i == 0 { - // first node is out of sync - node.On("StateAndLatest").Return(evmclient.NodeStateOutOfSync, int64(-1), nil) - } else { - // others are unreachable - node.On("StateAndLatest").Return(evmclient.NodeStateUnreachable, int64(1), big.NewInt(7)) - } - nodes = append(nodes, node) - } - - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - assert.Nil(t, selector.Select()) -} - -func TestTotalDifficultyNodeSelectorWithOrder(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - - t.Run("same td and order", func(t *testing.T) { - for i := 0; i < 3; i++ { - node := evmmocks.NewNode(t) - node.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(10)) - node.On("Order").Return(int32(2)) - nodes = append(nodes, node) - } - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - //Should select the first node because all things are equal - assert.Same(t, nodes[0], selector.Select()) - }) - - t.Run("same td but different order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), big.NewInt(10)) - node1.On("Order").Return(int32(3)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), big.NewInt(10)) - node2.On("Order").Return(int32(1)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(3), big.NewInt(10)) - node3.On("Order").Return(int32(2)) - - nodes := []evmclient.Node{node1, node2, node3} - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - //Should select the second node as it has the highest priority - assert.Same(t, nodes[1], selector.Select()) - }) - - t.Run("different td but same order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(10)) - node1.On("Order").Maybe().Return(int32(3)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(11)) - node2.On("Order").Maybe().Return(int32(3)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(12)) - node3.On("Order").Return(int32(3)) - - nodes := []evmclient.Node{node1, node2, node3} - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - //Should select the third node as it has the highest td - assert.Same(t, nodes[2], selector.Select()) - }) - - t.Run("different head and different order", func(t *testing.T) { - node1 := evmmocks.NewNode(t) - node1.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(100)) - node1.On("Order").Maybe().Return(int32(4)) - - node2 := evmmocks.NewNode(t) - node2.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(110)) - node2.On("Order").Maybe().Return(int32(5)) - - node3 := evmmocks.NewNode(t) - node3.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(110)) - node3.On("Order").Maybe().Return(int32(1)) - - node4 := evmmocks.NewNode(t) - node4.On("StateAndLatest").Return(evmclient.NodeStateAlive, int64(1), big.NewInt(105)) - node4.On("Order").Maybe().Return(int32(2)) - - nodes := []evmclient.Node{node1, node2, node3, node4} - selector := evmclient.NewTotalDifficultyNodeSelector(nodes) - //Should select the third node as it has the highest td and will win the priority tie-breaker - assert.Same(t, nodes[2], selector.Select()) - }) -} diff --git a/core/chains/evm/client/node_test.go b/core/chains/evm/client/node_test.go deleted file mode 100644 index a544fc0a3a2..00000000000 --- a/core/chains/evm/client/node_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package client_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -func Test_NodeWrapError(t *testing.T) { - t.Parallel() - - t.Run("handles nil errors", func(t *testing.T) { - err := evmclient.Wrap(nil, "foo") - assert.NoError(t, err) - }) - - t.Run("adds extra info to context deadline exceeded errors", func(t *testing.T) { - ctx, cancel := context.WithTimeout(testutils.Context(t), 0) - defer cancel() - - err := ctx.Err() - - err = evmclient.Wrap(err, "foo") - - assert.EqualError(t, err, "foo call failed: remote eth node timed out: context deadline exceeded") - }) -} diff --git a/core/chains/evm/client/pool.go b/core/chains/evm/client/pool.go deleted file mode 100644 index dcaf2a6b543..00000000000 --- a/core/chains/evm/client/pool.go +++ /dev/null @@ -1,503 +0,0 @@ -package client - -import ( - "context" - "fmt" - "math/big" - "sync" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - pkgerrors "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink-common/pkg/utils" - - "github.com/smartcontractkit/chainlink/v2/common/config" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" -) - -var ( - // PromEVMPoolRPCNodeStates reports current RPC node state - PromEVMPoolRPCNodeStates = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "evm_pool_rpc_node_states", - Help: "The number of RPC nodes currently in the given state for the given chain", - }, []string{"evmChainID", "state"}) -) - -const ( - NodeSelectionMode_HighestHead = "HighestHead" - NodeSelectionMode_RoundRobin = "RoundRobin" - NodeSelectionMode_TotalDifficulty = "TotalDifficulty" - NodeSelectionMode_PriorityLevel = "PriorityLevel" -) - -// NodeSelector represents a strategy to select the next node from the pool. -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NodeSelector] -type NodeSelector interface { - // Select returns a Node, or nil if none can be selected. - // Implementation must be thread-safe. - Select() Node - // Name returns the strategy name, e.g. "HighestHead" or "RoundRobin" - Name() string -} - -// PoolConfig represents settings for the Pool -// -// Deprecated: to be removed -type PoolConfig interface { - NodeSelectionMode() string - NodeNoNewHeadsThreshold() time.Duration - LeaseDuration() time.Duration -} - -// Pool represents an abstraction over one or more primary nodes -// It is responsible for liveness checking and balancing queries across live nodes -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.MultiNode] -type Pool struct { - services.StateMachine - nodes []Node - sendonlys []SendOnlyNode - chainID *big.Int - chainType config.ChainType - logger logger.SugaredLogger - selectionMode string - noNewHeadsThreshold time.Duration - nodeSelector NodeSelector - leaseDuration time.Duration - leaseTicker *time.Ticker - - activeMu sync.RWMutex - activeNode Node - - chStop services.StopChan - wg sync.WaitGroup -} - -// NewPool - creates new instance of [Pool] -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewMultiNode] -func NewPool(lggr logger.Logger, selectionMode string, leaseDuration time.Duration, noNewHeadsTreshold time.Duration, nodes []Node, sendonlys []SendOnlyNode, chainID *big.Int, chainType config.ChainType) *Pool { - if chainID == nil { - panic("chainID is required") - } - - nodeSelector := func() NodeSelector { - switch selectionMode { - case NodeSelectionMode_HighestHead: - return NewHighestHeadNodeSelector(nodes) - case NodeSelectionMode_RoundRobin: - return NewRoundRobinSelector(nodes) - case NodeSelectionMode_TotalDifficulty: - return NewTotalDifficultyNodeSelector(nodes) - case NodeSelectionMode_PriorityLevel: - return NewPriorityLevelNodeSelector(nodes) - default: - panic(fmt.Sprintf("unsupported NodeSelectionMode: %s", selectionMode)) - } - }() - - lggr = logger.Named(lggr, "Pool") - lggr = logger.With(lggr, "evmChainID", chainID.String()) - - p := &Pool{ - nodes: nodes, - sendonlys: sendonlys, - chainID: chainID, - chainType: chainType, - logger: logger.Sugared(lggr), - selectionMode: selectionMode, - noNewHeadsThreshold: noNewHeadsTreshold, - nodeSelector: nodeSelector, - chStop: make(chan struct{}), - leaseDuration: leaseDuration, - } - - p.logger.Debugf("The pool is configured to use NodeSelectionMode: %s", selectionMode) - - return p -} - -// Dial starts every node in the pool -// -// Nodes handle their own redialing and runloops, so this function does not -// return any error if the nodes aren't available -func (p *Pool) Dial(ctx context.Context) error { - return p.StartOnce("Pool", func() (merr error) { - if len(p.nodes) == 0 { - return pkgerrors.Errorf("no available nodes for chain %s", p.chainID.String()) - } - var ms services.MultiStart - for _, n := range p.nodes { - if n.ChainID().Cmp(p.chainID) != 0 { - return ms.CloseBecause(pkgerrors.Errorf("node %s has chain ID %s which does not match pool chain ID of %s", n.String(), n.ChainID().String(), p.chainID.String())) - } - rawNode, ok := n.(*node) - if ok { - // This is a bit hacky but it allows the node to be aware of - // pool state and prevent certain state transitions that might - // otherwise leave no nodes available. It is better to have one - // node in a degraded state than no nodes at all. - rawNode.nLiveNodes = p.nLiveNodes - } - // node will handle its own redialing and automatic recovery - if err := ms.Start(ctx, n); err != nil { - return err - } - } - for _, s := range p.sendonlys { - if s.ChainID().Cmp(p.chainID) != 0 { - return ms.CloseBecause(pkgerrors.Errorf("sendonly node %s has chain ID %s which does not match pool chain ID of %s", s.String(), s.ChainID().String(), p.chainID.String())) - } - if err := ms.Start(ctx, s); err != nil { - return err - } - } - p.wg.Add(1) - go p.runLoop() - - if p.leaseDuration.Seconds() > 0 && p.selectionMode != NodeSelectionMode_RoundRobin { - p.logger.Infof("The pool will switch to best node every %s", p.leaseDuration.String()) - p.wg.Add(1) - go p.checkLeaseLoop() - } else { - p.logger.Info("Best node switching is disabled") - } - - return nil - }) -} - -// nLiveNodes returns the number of currently alive nodes, as well as the highest block number and greatest total difficulty. -// totalDifficulty will be 0 if all nodes return nil. -func (p *Pool) nLiveNodes() (nLiveNodes int, blockNumber int64, totalDifficulty *big.Int) { - totalDifficulty = big.NewInt(0) - for _, n := range p.nodes { - if s, num, td := n.StateAndLatest(); s == NodeStateAlive { - nLiveNodes++ - if num > blockNumber { - blockNumber = num - } - if td != nil && td.Cmp(totalDifficulty) > 0 { - totalDifficulty = td - } - } - } - return -} - -func (p *Pool) checkLease() { - bestNode := p.nodeSelector.Select() - for _, n := range p.nodes { - // Terminate client subscriptions. Services are responsible for reconnecting, which will be routed to the new - // best node. Only terminate connections with more than 1 subscription to account for the aliveLoop subscription - if n.State() == NodeStateAlive && n != bestNode && n.SubscribersCount() > 1 { - p.logger.Infof("Switching to best node from %q to %q", n.String(), bestNode.String()) - n.UnsubscribeAllExceptAliveLoop() - } - } - - if bestNode != p.activeNode { - p.activeMu.Lock() - p.activeNode = bestNode - p.activeMu.Unlock() - } -} - -func (p *Pool) checkLeaseLoop() { - defer p.wg.Done() - p.leaseTicker = time.NewTicker(p.leaseDuration) - defer p.leaseTicker.Stop() - - for { - select { - case <-p.leaseTicker.C: - p.checkLease() - case <-p.chStop: - return - } - } -} - -func (p *Pool) runLoop() { - defer p.wg.Done() - - p.report() - - // Prometheus' default interval is 15s, set this to under 7.5s to avoid - // aliasing (see: https://en.wikipedia.org/wiki/Nyquist_frequency) - reportInterval := 6500 * time.Millisecond - monitor := time.NewTicker(utils.WithJitter(reportInterval)) - defer monitor.Stop() - - for { - select { - case <-monitor.C: - p.report() - case <-p.chStop: - return - } - } -} - -func (p *Pool) report() { - type nodeWithState struct { - Node string - State string - } - - var total, dead int - counts := make(map[NodeState]int) - nodeStates := make([]nodeWithState, len(p.nodes)) - for i, n := range p.nodes { - state := n.State() - nodeStates[i] = nodeWithState{n.String(), state.String()} - total++ - if state != NodeStateAlive { - dead++ - } - counts[state]++ - } - for _, state := range allNodeStates { - count := counts[state] - PromEVMPoolRPCNodeStates.WithLabelValues(p.chainID.String(), state.String()).Set(float64(count)) - } - - live := total - dead - p.logger.Tracew(fmt.Sprintf("Pool state: %d/%d nodes are alive", live, total), "nodeStates", nodeStates) - if total == dead { - rerr := fmt.Errorf("no EVM primary nodes available: 0/%d nodes are alive", total) - p.logger.Criticalw(rerr.Error(), "nodeStates", nodeStates) - p.SvcErrBuffer.Append(rerr) - } else if dead > 0 { - p.logger.Errorw(fmt.Sprintf("At least one EVM primary node is dead: %d/%d nodes are alive", live, total), "nodeStates", nodeStates) - } -} - -// Close tears down the pool and closes all nodes -func (p *Pool) Close() error { - return p.StopOnce("Pool", func() error { - close(p.chStop) - p.wg.Wait() - - return services.CloseAll(services.MultiCloser(p.nodes), services.MultiCloser(p.sendonlys)) - }) -} - -func (p *Pool) ChainID() *big.Int { - return p.selectNode().ChainID() -} - -func (p *Pool) ChainType() config.ChainType { - return p.chainType -} - -// selectNode returns the active Node, if it is still NodeStateAlive, otherwise it selects a new one from the NodeSelector. -func (p *Pool) selectNode() (node Node) { - p.activeMu.RLock() - node = p.activeNode - p.activeMu.RUnlock() - if node != nil && node.State() == NodeStateAlive { - return // still alive - } - - // select a new one - p.activeMu.Lock() - defer p.activeMu.Unlock() - node = p.activeNode - if node != nil && node.State() == NodeStateAlive { - return // another goroutine beat us here - } - - p.activeNode = p.nodeSelector.Select() - - if p.activeNode == nil { - p.logger.Criticalw("No live RPC nodes available", "NodeSelectionMode", p.nodeSelector.Name()) - errmsg := fmt.Errorf("no live nodes available for chain %s", p.chainID.String()) - p.SvcErrBuffer.Append(errmsg) - return &erroringNode{errMsg: errmsg.Error()} - } - - if p.leaseTicker != nil { - p.leaseTicker.Reset(p.leaseDuration) - } - return p.activeNode -} - -func (p *Pool) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - return p.selectNode().CallContext(ctx, result, method, args...) -} - -func (p *Pool) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - return p.selectNode().BatchCallContext(ctx, b) -} - -// BatchCallContextAll calls BatchCallContext for every single node including -// sendonlys. -// CAUTION: This should only be used for mass re-transmitting transactions, it -// might have unexpected effects to use it for anything else. -func (p *Pool) BatchCallContextAll(ctx context.Context, b []rpc.BatchElem) error { - var wg sync.WaitGroup - defer wg.Wait() - - main := p.selectNode() - var all []SendOnlyNode - for _, n := range p.nodes { - all = append(all, n) - } - all = append(all, p.sendonlys...) - for _, n := range all { - if n == main { - // main node is used at the end for the return value - continue - } - // Parallel call made to all other nodes with ignored return value - wg.Add(1) - go func(n SendOnlyNode) { - defer wg.Done() - err := n.BatchCallContext(ctx, b) - if err != nil { - p.logger.Debugw("Secondary node BatchCallContext failed", "err", err) - } else { - p.logger.Trace("Secondary node BatchCallContext success") - } - }(n) - } - - return main.BatchCallContext(ctx, b) -} - -// SendTransaction wrapped Geth client methods -func (p *Pool) SendTransaction(ctx context.Context, tx *types.Transaction) error { - main := p.selectNode() - var all []SendOnlyNode - for _, n := range p.nodes { - all = append(all, n) - } - all = append(all, p.sendonlys...) - for _, n := range all { - if n == main { - // main node is used at the end for the return value - continue - } - // Parallel send to all other nodes with ignored return value - // Async - we do not want to block the main thread with secondary nodes - // in case they are unreliable/slow. - // It is purely a "best effort" send. - // Resource is not unbounded because the default context has a timeout. - ok := p.IfNotStopped(func() { - // Must wrap inside IfNotStopped to avoid waitgroup racing with Close - p.wg.Add(1) - go func(n SendOnlyNode) { - defer p.wg.Done() - - sendCtx, cancel := p.chStop.CtxCancel(ContextWithDefaultTimeout()) - defer cancel() - - err := NewSendError(n.SendTransaction(sendCtx, tx)) - p.logger.Debugw("Sendonly node sent transaction", "name", n.String(), "tx", tx, "err", err) - if err == nil || err.IsNonceTooLowError(nil) || err.IsTransactionAlreadyMined(nil) || err.IsTransactionAlreadyInMempool(nil) { - // Nonce too low or transaction known errors are expected since - // the primary SendTransaction may well have succeeded already - return - } - - p.logger.Warnw("Eth client returned error", "name", n.String(), "err", err, "tx", tx) - }(n) - }) - if !ok { - p.logger.Debug("Cannot send transaction on sendonly node; pool is stopped", "node", n.String()) - } - } - - return main.SendTransaction(ctx, tx) -} - -func (p *Pool) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { - return p.selectNode().PendingCodeAt(ctx, account) -} - -func (p *Pool) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { - return p.selectNode().PendingNonceAt(ctx, account) -} - -func (p *Pool) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { - return p.selectNode().NonceAt(ctx, account, blockNumber) -} - -func (p *Pool) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { - return p.selectNode().TransactionReceipt(ctx, txHash) -} - -func (p *Pool) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) { - return p.selectNode().TransactionByHash(ctx, txHash) -} - -func (p *Pool) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - return p.selectNode().BlockByNumber(ctx, number) -} - -func (p *Pool) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - return p.selectNode().BlockByHash(ctx, hash) -} - -func (p *Pool) BlockNumber(ctx context.Context) (uint64, error) { - return p.selectNode().BlockNumber(ctx) -} - -func (p *Pool) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { - return p.selectNode().BalanceAt(ctx, account, blockNumber) -} - -func (p *Pool) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { - return p.selectNode().FilterLogs(ctx, q) -} - -func (p *Pool) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - return p.selectNode().SubscribeFilterLogs(ctx, q, ch) -} - -func (p *Pool) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { - return p.selectNode().EstimateGas(ctx, call) -} - -func (p *Pool) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - return p.selectNode().SuggestGasPrice(ctx) -} - -func (p *Pool) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - return p.selectNode().CallContract(ctx, msg, blockNumber) -} - -func (p *Pool) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { - return p.selectNode().PendingCallContract(ctx, msg) -} - -func (p *Pool) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { - return p.selectNode().CodeAt(ctx, account, blockNumber) -} - -// bind.ContractBackend methods -func (p *Pool) HeaderByNumber(ctx context.Context, n *big.Int) (*types.Header, error) { - return p.selectNode().HeaderByNumber(ctx, n) -} -func (p *Pool) HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) { - return p.selectNode().HeaderByHash(ctx, h) -} - -func (p *Pool) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - return p.selectNode().SuggestGasTipCap(ctx) -} - -// EthSubscribe implements evmclient.Client -func (p *Pool) EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) { - return p.selectNode().EthSubscribe(ctx, channel, args...) -} diff --git a/core/chains/evm/client/pool_test.go b/core/chains/evm/client/pool_test.go deleted file mode 100644 index 5a2c13130d3..00000000000 --- a/core/chains/evm/client/pool_test.go +++ /dev/null @@ -1,393 +0,0 @@ -package client_test - -import ( - "context" - "math/big" - "net/http/httptest" - "net/url" - "sync" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/rpc" - promtestutil "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" - "go.uber.org/zap" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/mocks" - "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -type poolConfig struct { - selectionMode string - noNewHeadsThreshold time.Duration - leaseDuration time.Duration -} - -func (c poolConfig) NodeSelectionMode() string { - return c.selectionMode -} - -func (c poolConfig) NodeNoNewHeadsThreshold() time.Duration { - return c.noNewHeadsThreshold -} - -func (c poolConfig) LeaseDuration() time.Duration { - return c.leaseDuration -} - -var defaultConfig evmclient.PoolConfig = &poolConfig{ - selectionMode: evmclient.NodeSelectionMode_RoundRobin, - noNewHeadsThreshold: 0, - leaseDuration: time.Second * 0, -} - -func TestPool_Dial(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - poolChainID *big.Int - nodeChainID int64 - sendNodeChainID int64 - nodes []chainIDResps - sendNodes []chainIDResp - errStr string - }{ - { - name: "no nodes", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{}, - sendNodes: []chainIDResp{}, - errStr: "no available nodes for chain 0", - }, - { - name: "normal", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{ - {ws: chainIDResp{testutils.FixtureChainID.Int64(), nil}}, - }, - sendNodes: []chainIDResp{ - {testutils.FixtureChainID.Int64(), nil}, - }, - }, - { - name: "node has wrong chain ID compared to pool", - poolChainID: testutils.FixtureChainID, - nodeChainID: 42, - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{ - {ws: chainIDResp{1, nil}}, - }, - sendNodes: []chainIDResp{ - {1, nil}, - }, - errStr: "has chain ID 42 which does not match pool chain ID of 0", - }, - { - name: "sendonly node has wrong chain ID compared to pool", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: 42, - nodes: []chainIDResps{ - {ws: chainIDResp{testutils.FixtureChainID.Int64(), nil}}, - }, - sendNodes: []chainIDResp{ - {testutils.FixtureChainID.Int64(), nil}, - }, - errStr: "has chain ID 42 which does not match pool chain ID of 0", - }, - { - name: "remote RPC has wrong chain ID for primary node (ws) - no error, it will go into retry loop", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{ - { - ws: chainIDResp{42, nil}, - http: &chainIDResp{testutils.FixtureChainID.Int64(), nil}, - }, - }, - sendNodes: []chainIDResp{ - {testutils.FixtureChainID.Int64(), nil}, - }, - }, - { - name: "remote RPC has wrong chain ID for primary node (http) - no error, it will go into retry loop", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{ - { - ws: chainIDResp{testutils.FixtureChainID.Int64(), nil}, - http: &chainIDResp{42, nil}, - }, - }, - sendNodes: []chainIDResp{ - {testutils.FixtureChainID.Int64(), nil}, - }, - }, - { - name: "remote RPC has wrong chain ID for sendonly node - no error, it will go into retry loop", - poolChainID: testutils.FixtureChainID, - nodeChainID: testutils.FixtureChainID.Int64(), - sendNodeChainID: testutils.FixtureChainID.Int64(), - nodes: []chainIDResps{ - {ws: chainIDResp{testutils.FixtureChainID.Int64(), nil}}, - }, - sendNodes: []chainIDResp{ - {42, nil}, - }, - }, - } - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - ctx := testutils.Context(t) - - nodes := make([]evmclient.Node, len(test.nodes)) - for i, n := range test.nodes { - nodes[i] = n.newNode(t, test.nodeChainID) - } - sendNodes := make([]evmclient.SendOnlyNode, len(test.sendNodes)) - for i, n := range test.sendNodes { - sendNodes[i] = n.newSendOnlyNode(t, test.sendNodeChainID) - } - p := evmclient.NewPool(logger.Test(t), defaultConfig.NodeSelectionMode(), defaultConfig.LeaseDuration(), time.Second*0, nodes, sendNodes, test.poolChainID, "") - err := p.Dial(ctx) - if err == nil { - t.Cleanup(func() { assert.NoError(t, p.Close()) }) - } - assert.False(t, p.ChainType().IsL2()) - if test.errStr != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), test.errStr) - } else { - require.NoError(t, err) - } - }) - } -} - -type chainIDResp struct { - chainID int64 - err error -} - -func (r *chainIDResp) newSendOnlyNode(t *testing.T, nodeChainID int64) evmclient.SendOnlyNode { - httpURL := r.newHTTPServer(t) - return evmclient.NewSendOnlyNode(logger.Test(t), *httpURL, t.Name(), big.NewInt(nodeChainID)) -} - -func (r *chainIDResp) newHTTPServer(t *testing.T) *url.URL { - rpcSrv := rpc.NewServer() - t.Cleanup(rpcSrv.Stop) - err := rpcSrv.RegisterName("eth", &chainIDService{*r}) - require.NoError(t, err) - ts := httptest.NewServer(rpcSrv) - t.Cleanup(ts.Close) - - httpURL, err := url.Parse(ts.URL) - require.NoError(t, err) - return httpURL -} - -type chainIDResps struct { - ws chainIDResp - http *chainIDResp - id int32 -} - -func (r *chainIDResps) newNode(t *testing.T, nodeChainID int64) evmclient.Node { - ws := testutils.NewWSServer(t, big.NewInt(r.ws.chainID), func(method string, params gjson.Result) (resp testutils.JSONRPCResponse) { - switch method { - case "eth_subscribe": - resp.Result = `"0x00"` - resp.Notify = headResult - return - case "eth_unsubscribe": - resp.Result = "true" - return - } - t.Errorf("Unexpected method call: %s(%s)", method, params) - return - }).WSURL().String() - - wsURL, err := url.Parse(ws) - require.NoError(t, err) - - var httpURL *url.URL - if r.http != nil { - httpURL = r.http.newHTTPServer(t) - } - - defer func() { r.id++ }() - return evmclient.NewNode(evmclient.TestNodePoolConfig{}, time.Second*0, logger.Test(t), *wsURL, httpURL, t.Name(), r.id, big.NewInt(nodeChainID), 0) -} - -type chainIDService struct { - chainIDResp -} - -func (x *chainIDService) ChainId(ctx context.Context) (*hexutil.Big, error) { - if x.err != nil { - return nil, x.err - } - return (*hexutil.Big)(big.NewInt(x.chainID)), nil -} - -func TestUnit_Pool_RunLoop(t *testing.T) { - t.Parallel() - - n1 := evmmocks.NewNode(t) - n2 := evmmocks.NewNode(t) - n3 := evmmocks.NewNode(t) - nodes := []evmclient.Node{n1, n2, n3} - - lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) - p := evmclient.NewPool(lggr, defaultConfig.NodeSelectionMode(), defaultConfig.LeaseDuration(), time.Second*0, nodes, []evmclient.SendOnlyNode{}, &cltest.FixtureChainID, "") - - n1.On("String").Maybe().Return("n1") - n2.On("String").Maybe().Return("n2") - n3.On("String").Maybe().Return("n3") - - n1.On("Close").Maybe().Return(nil) - n2.On("Close").Maybe().Return(nil) - n3.On("Close").Maybe().Return(nil) - - // n1 is alive - n1.On("Start", mock.Anything).Return(nil).Once() - n1.On("State").Return(evmclient.NodeStateAlive) - n1.On("ChainID").Return(testutils.FixtureChainID).Once() - // n2 is unreachable - n2.On("Start", mock.Anything).Return(nil).Once() - n2.On("State").Return(evmclient.NodeStateUnreachable) - n2.On("ChainID").Return(testutils.FixtureChainID).Once() - // n3 is out of sync - n3.On("Start", mock.Anything).Return(nil).Once() - n3.On("State").Return(evmclient.NodeStateOutOfSync) - n3.On("ChainID").Return(testutils.FixtureChainID).Once() - - require.NoError(t, p.Dial(testutils.Context(t))) - t.Cleanup(func() { assert.NoError(t, p.Close()) }) - - testutils.WaitForLogMessage(t, observedLogs, "At least one EVM primary node is dead") - - testutils.AssertEventually(t, func() bool { - totalReported := promtestutil.CollectAndCount(evmclient.PromEVMPoolRPCNodeStates) - if totalReported < 3 { - return false - } - if promtestutil.ToFloat64(evmclient.PromEVMPoolRPCNodeStates.WithLabelValues("0", "Alive")) < 1.0 { - return false - } - if promtestutil.ToFloat64(evmclient.PromEVMPoolRPCNodeStates.WithLabelValues("0", "Unreachable")) < 1.0 { - return false - } - if promtestutil.ToFloat64(evmclient.PromEVMPoolRPCNodeStates.WithLabelValues("0", "OutOfSync")) < 1.0 { - return false - } - return true - }) -} - -func TestUnit_Pool_BatchCallContextAll(t *testing.T) { - t.Parallel() - - var nodes []evmclient.Node - var sendonlys []evmclient.SendOnlyNode - - nodeCount := 2 - sendOnlyCount := 3 - - b := []rpc.BatchElem{ - {Method: "method", Args: []interface{}{1, false}}, - {Method: "method2"}, - } - - ctx := testutils.Context(t) - - for i := 0; i < nodeCount; i++ { - node := evmmocks.NewNode(t) - node.On("State").Return(evmclient.NodeStateAlive).Maybe() - node.On("BatchCallContext", ctx, b).Return(nil).Once() - nodes = append(nodes, node) - } - for i := 0; i < sendOnlyCount; i++ { - s := evmmocks.NewSendOnlyNode(t) - s.On("BatchCallContext", ctx, b).Return(nil).Once() - sendonlys = append(sendonlys, s) - } - - p := evmclient.NewPool(logger.Test(t), defaultConfig.NodeSelectionMode(), defaultConfig.LeaseDuration(), time.Second*0, nodes, sendonlys, &cltest.FixtureChainID, "") - - assert.False(t, p.ChainType().IsL2()) - require.NoError(t, p.BatchCallContextAll(ctx, b)) -} - -func TestUnit_Pool_LeaseDuration(t *testing.T) { - t.Parallel() - - n1 := evmmocks.NewNode(t) - n2 := evmmocks.NewNode(t) - nodes := []evmclient.Node{n1, n2} - type nodeStateSwitch struct { - isAlive bool - mu sync.RWMutex - } - - nodeSwitch := nodeStateSwitch{ - isAlive: true, - mu: sync.RWMutex{}, - } - - n1.On("String").Maybe().Return("n1") - n2.On("String").Maybe().Return("n2") - n1.On("Close").Maybe().Return(nil) - n2.On("Close").Maybe().Return(nil) - n2.On("UnsubscribeAllExceptAliveLoop").Return() - n2.On("SubscribersCount").Return(int32(2)) - - n1.On("Start", mock.Anything).Return(nil).Once() - n1.On("State").Return(func() evmclient.NodeState { - nodeSwitch.mu.RLock() - defer nodeSwitch.mu.RUnlock() - if nodeSwitch.isAlive { - return evmclient.NodeStateAlive - } - return evmclient.NodeStateOutOfSync - }) - n1.On("Order").Return(int32(1)) - n1.On("ChainID").Return(testutils.FixtureChainID).Once() - - n2.On("Start", mock.Anything).Return(nil).Once() - n2.On("State").Return(evmclient.NodeStateAlive) - n2.On("Order").Return(int32(2)) - n2.On("ChainID").Return(testutils.FixtureChainID).Once() - - lggr, observedLogs := logger.TestObserved(t, zap.InfoLevel) - p := evmclient.NewPool(lggr, "PriorityLevel", time.Second*2, time.Second*0, nodes, []evmclient.SendOnlyNode{}, &cltest.FixtureChainID, "") - require.NoError(t, p.Dial(testutils.Context(t))) - t.Cleanup(func() { assert.NoError(t, p.Close()) }) - - testutils.WaitForLogMessage(t, observedLogs, "The pool will switch to best node every 2s") - nodeSwitch.mu.Lock() - nodeSwitch.isAlive = false - nodeSwitch.mu.Unlock() - testutils.WaitForLogMessage(t, observedLogs, "At least one EVM primary node is dead") - nodeSwitch.mu.Lock() - nodeSwitch.isAlive = true - nodeSwitch.mu.Unlock() - testutils.WaitForLogMessage(t, observedLogs, `Switching to best node from "n2" to "n1"`) -} diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index 548acf3206c..5b64900a0cb 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -17,9 +17,12 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/google/uuid" pkgerrors "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" commonassets "github.com/smartcontractkit/chainlink-common/pkg/assets" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" commonclient "github.com/smartcontractkit/chainlink/v2/common/client" commontypes "github.com/smartcontractkit/chainlink/v2/common/types" @@ -29,6 +32,48 @@ import ( ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" ) +var ( + promEVMPoolRPCNodeDials = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_dials_total", + Help: "The total number of dials for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + promEVMPoolRPCNodeDialsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_dials_failed", + Help: "The total number of failed dials for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + promEVMPoolRPCNodeDialsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_dials_success", + Help: "The total number of successful dials for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + + promEVMPoolRPCNodeCalls = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_calls_total", + Help: "The approximate total number of RPC calls for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + promEVMPoolRPCNodeCallsFailed = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_calls_failed", + Help: "The approximate total number of failed RPC calls for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + promEVMPoolRPCNodeCallsSuccess = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "evm_pool_rpc_node_calls_success", + Help: "The approximate total number of successful RPC calls for the given RPC node", + }, []string{"evmChainID", "nodeName"}) + promEVMPoolRPCCallTiming = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "evm_pool_rpc_node_rpc_call_time", + Help: "The duration of an RPC call in nanoseconds", + Buckets: []float64{ + float64(50 * time.Millisecond), + float64(100 * time.Millisecond), + float64(200 * time.Millisecond), + float64(500 * time.Millisecond), + float64(1 * time.Second), + float64(2 * time.Second), + float64(4 * time.Second), + float64(8 * time.Second), + }, + }, []string{"evmChainID", "nodeName", "rpcHost", "isSendOnly", "success", "rpcCallName"}) +) + // RPCClient includes all the necessary generalized RPC methods along with any additional chain-specific methods. // //go:generate mockery --quiet --name RPCClient --output ./mocks --case=underscore @@ -58,6 +103,12 @@ type RPCClient interface { TransactionReceiptGeth(ctx context.Context, txHash common.Hash) (r *types.Receipt, err error) } +type rawclient struct { + rpc *rpc.Client + geth *ethclient.Client + uri url.URL +} + type rpcClient struct { rpcLog logger.SugaredLogger name string @@ -837,6 +888,15 @@ func (r *rpcClient) BalanceAt(ctx context.Context, account common.Address, block return } +// CallArgs represents the data used to call the balance method of a contract. +// "To" is the address of the ERC contract. "Data" is the message sent +// to the contract. "From" is the sender address. +type CallArgs struct { + From common.Address `json:"from"` + To common.Address `json:"to"` + Data hexutil.Bytes `json:"data"` +} + // TokenBalance returns the balance of the given address for the token contract address. func (r *rpcClient) TokenBalance(ctx context.Context, address common.Address, contractAddress common.Address) (*big.Int, error) { result := "" @@ -1014,6 +1074,21 @@ func (r *rpcClient) makeLiveQueryCtxAndSafeGetClients(parentCtx context.Context) return } +// makeQueryCtx returns a context that cancels if: +// 1. Passed in ctx cancels +// 2. Passed in channel is closed +// 3. Default timeout is reached (queryTimeout) +func makeQueryCtx(ctx context.Context, ch services.StopChan) (context.Context, context.CancelFunc) { + var chCancel, timeoutCancel context.CancelFunc + ctx, chCancel = ch.Ctx(ctx) + ctx, timeoutCancel = context.WithTimeout(ctx, queryTimeout) + cancel := func() { + chCancel() + timeoutCancel() + } + return ctx, cancel +} + func (r *rpcClient) makeQueryCtx(ctx context.Context) (context.Context, context.CancelFunc) { return makeQueryCtx(ctx, r.getChStopInflight()) } @@ -1058,3 +1133,10 @@ func (r *rpcClient) Name() string { func Name(r *rpcClient) string { return r.name } + +func ToBlockNumArg(number *big.Int) string { + if number == nil { + return "latest" + } + return hexutil.EncodeBig(number) +} diff --git a/core/chains/evm/client/send_only_node.go b/core/chains/evm/client/send_only_node.go deleted file mode 100644 index b6ad26696fc..00000000000 --- a/core/chains/evm/client/send_only_node.go +++ /dev/null @@ -1,267 +0,0 @@ -package client - -import ( - "context" - "fmt" - "log" - "math/big" - "net/url" - "strconv" - "sync" - "time" - - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/rpc" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" -) - -//go:generate mockery --quiet --name SendOnlyNode --output ../mocks/ --case=underscore - -// SendOnlyNode represents one ethereum node used as a sendonly -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.SendOnlyNode] -type SendOnlyNode interface { - // Start may attempt to connect to the node, but should only return error for misconfiguration - never for temporary errors. - Start(context.Context) error - Close() error - - ChainID() (chainID *big.Int) - - SendTransaction(ctx context.Context, tx *types.Transaction) error - BatchCallContext(ctx context.Context, b []rpc.BatchElem) error - - String() string - // State returns NodeState - State() NodeState - // Name is a unique identifier for this node. - Name() string -} - -//go:generate mockery --quiet --name TxSender --output ./mocks/ --case=underscore - -type TxSender interface { - SendTransaction(ctx context.Context, tx *types.Transaction) error - ChainID(context.Context) (*big.Int, error) -} - -//go:generate mockery --quiet --name BatchSender --output ./mocks/ --case=underscore - -type BatchSender interface { - BatchCallContext(ctx context.Context, b []rpc.BatchElem) error -} - -var _ SendOnlyNode = &sendOnlyNode{} - -// It only supports sending transactions -// It must a http(s) url -type sendOnlyNode struct { - services.StateMachine - - stateMu sync.RWMutex // protects state* fields - state NodeState - - uri url.URL - batchSender BatchSender - sender TxSender - log logger.Logger - dialed bool - name string - chainID *big.Int - chStop services.StopChan - wg sync.WaitGroup -} - -// NewSendOnlyNode returns a new sendonly node -// -// Deprecated: use [pkg/github.com/smartcontractkit/chainlink/v2/common/client.NewSendOnlyNode] -func NewSendOnlyNode(lggr logger.Logger, httpuri url.URL, name string, chainID *big.Int) SendOnlyNode { - s := new(sendOnlyNode) - s.name = name - s.log = logger.Named(logger.Named(lggr, "SendOnlyNode"), name) - s.log = logger.With(s.log, - "nodeTier", "sendonly", - ) - s.uri = httpuri - s.chainID = chainID - s.chStop = make(chan struct{}) - return s -} - -func (s *sendOnlyNode) Start(ctx context.Context) error { - return s.StartOnce(s.name, func() error { - s.start(ctx) - return nil - }) -} - -// Start setups up and verifies the sendonly node -// Should only be called once in a node's lifecycle -func (s *sendOnlyNode) start(startCtx context.Context) { - if s.state != NodeStateUndialed { - panic(fmt.Sprintf("cannot dial node with state %v", s.state)) - } - - s.log.Debugw("evmclient.Client#Dial(...)") - if s.dialed { - panic("evmclient.Client.Dial(...) should only be called once during the node's lifetime.") - } - - // DialHTTP doesn't actually make any external HTTP calls - // It can only return error if the URL is malformed. No amount of retries - // will change this result. - rpc, err := rpc.DialHTTP(s.uri.String()) - if err != nil { - promEVMPoolRPCNodeTransitionsToUnusable.WithLabelValues(s.chainID.String(), s.name).Inc() - s.log.Errorw("Dial failed: EVM SendOnly Node is unusable", "err", err) - s.setState(NodeStateUnusable) - return - } - s.dialed = true - geth := ethclient.NewClient(rpc) - s.SetEthClient(rpc, geth) - - if s.chainID.Cmp(big.NewInt(0)) == 0 { - // Skip verification if chainID is zero - s.log.Warn("sendonly rpc ChainID verification skipped") - } else { - verifyCtx, verifyCancel := s.makeQueryCtx(startCtx) - defer verifyCancel() - - chainID, err := s.sender.ChainID(verifyCtx) - if err != nil || chainID.Cmp(s.chainID) != 0 { - promEVMPoolRPCNodeTransitionsToUnreachable.WithLabelValues(s.chainID.String(), s.name).Inc() - if err != nil { - promEVMPoolRPCNodeTransitionsToUnreachable.WithLabelValues(s.chainID.String(), s.name).Inc() - s.log.Errorw(fmt.Sprintf("Verify failed: %v", err), "err", err) - s.setState(NodeStateUnreachable) - } else { - promEVMPoolRPCNodeTransitionsToInvalidChainID.WithLabelValues(s.chainID.String(), s.name).Inc() - s.log.Errorf( - "sendonly rpc ChainID doesn't match local chain ID: RPC ID=%s, local ID=%s, node name=%s", - chainID.String(), - s.chainID.String(), - s.name, - ) - s.setState(NodeStateInvalidChainID) - } - // Since it has failed, spin up the verifyLoop that will keep - // retrying until success - s.wg.Add(1) - go s.verifyLoop() - return - } - } - - promEVMPoolRPCNodeTransitionsToAlive.WithLabelValues(s.chainID.String(), s.name).Inc() - s.setState(NodeStateAlive) - s.log.Infow("Sendonly RPC Node is online", "nodeState", s.state) -} - -func (s *sendOnlyNode) SetEthClient(newBatchSender BatchSender, newSender TxSender) { - if s.sender != nil { - log.Panicf("sendOnlyNode.SetEthClient should only be called once!") - return - } - s.batchSender = newBatchSender - s.sender = newSender -} - -func (s *sendOnlyNode) Close() error { - return s.StopOnce(s.name, func() error { - close(s.chStop) - s.wg.Wait() - s.setState(NodeStateClosed) - return nil - }) -} - -func (s *sendOnlyNode) logTiming(lggr logger.Logger, duration time.Duration, err error, callName string) { - promEVMPoolRPCCallTiming. - WithLabelValues( - s.chainID.String(), // chain id - s.name, // node name - s.uri.Host, // rpc domain - "true", // is send only - strconv.FormatBool(err == nil), // is successful - callName, // rpc call name - ). - Observe(float64(duration)) - lggr.Debugw(fmt.Sprintf("SendOnly RPC call: evmclient.#%s", callName), - "duration", duration, - "rpcDomain", s.uri.Host, - "name", s.name, - "chainID", s.chainID, - "sendOnly", true, - "err", err, - ) -} - -func (s *sendOnlyNode) SendTransaction(parentCtx context.Context, tx *types.Transaction) (err error) { - defer func(start time.Time) { - s.logTiming(s.log, time.Since(start), err, "SendTransaction") - }(time.Now()) - - ctx, cancel := s.makeQueryCtx(parentCtx) - defer cancel() - return s.wrap(s.sender.SendTransaction(ctx, tx)) -} - -func (s *sendOnlyNode) BatchCallContext(parentCtx context.Context, b []rpc.BatchElem) (err error) { - defer func(start time.Time) { - s.logTiming(logger.With(s.log, "nBatchElems", len(b)), time.Since(start), err, "BatchCallContext") - }(time.Now()) - - ctx, cancel := s.makeQueryCtx(parentCtx) - defer cancel() - return s.wrap(s.batchSender.BatchCallContext(ctx, b)) -} - -func (s *sendOnlyNode) ChainID() (chainID *big.Int) { - return s.chainID -} - -func (s *sendOnlyNode) wrap(err error) error { - return wrap(err, fmt.Sprintf("sendonly http (%s)", s.uri.Redacted())) -} - -func (s *sendOnlyNode) String() string { - return fmt.Sprintf("(secondary)%s:%s", s.name, s.uri.Redacted()) -} - -// makeQueryCtx returns a context that cancels if: -// 1. Passed in ctx cancels -// 2. chStop is closed -// 3. Default timeout is reached (queryTimeout) -func (s *sendOnlyNode) makeQueryCtx(ctx context.Context) (context.Context, context.CancelFunc) { - var chCancel, timeoutCancel context.CancelFunc - ctx, chCancel = s.chStop.Ctx(ctx) - ctx, timeoutCancel = context.WithTimeout(ctx, queryTimeout) - cancel := func() { - chCancel() - timeoutCancel() - } - return ctx, cancel -} - -func (s *sendOnlyNode) setState(state NodeState) (changed bool) { - s.stateMu.Lock() - defer s.stateMu.Unlock() - if s.state == state { - return false - } - s.state = state - return true -} - -func (s *sendOnlyNode) State() NodeState { - s.stateMu.RLock() - defer s.stateMu.RUnlock() - return s.state -} - -func (s *sendOnlyNode) Name() string { - return s.name -} diff --git a/core/chains/evm/client/send_only_node_lifecycle.go b/core/chains/evm/client/send_only_node_lifecycle.go deleted file mode 100644 index 127a5c6678c..00000000000 --- a/core/chains/evm/client/send_only_node_lifecycle.go +++ /dev/null @@ -1,68 +0,0 @@ -package client - -import ( - "fmt" - "time" - - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" -) - -// verifyLoop may only be triggered once, on Start, if initial chain ID check -// fails. -// -// It will continue checking until success and then exit permanently. -func (s *sendOnlyNode) verifyLoop() { - defer s.wg.Done() - ctx, cancel := s.chStop.NewCtx() - defer cancel() - - backoff := utils.NewRedialBackoff() - for { - select { - case <-time.After(backoff.Duration()): - chainID, err := s.sender.ChainID(ctx) - if err != nil { - ok := s.IfStarted(func() { - if changed := s.setState(NodeStateUnreachable); changed { - promEVMPoolRPCNodeTransitionsToUnreachable.WithLabelValues(s.chainID.String(), s.name).Inc() - } - }) - if !ok { - return - } - s.log.Errorw(fmt.Sprintf("Verify failed: %v", err), "err", err) - continue - } else if chainID.Cmp(s.chainID) != 0 { - ok := s.IfStarted(func() { - if changed := s.setState(NodeStateInvalidChainID); changed { - promEVMPoolRPCNodeTransitionsToInvalidChainID.WithLabelValues(s.chainID.String(), s.name).Inc() - } - }) - if !ok { - return - } - s.log.Errorf( - "sendonly rpc ChainID doesn't match local chain ID: RPC ID=%s, local ID=%s, node name=%s", - chainID.String(), - s.chainID.String(), - s.name, - ) - - continue - } else { - ok := s.IfStarted(func() { - if changed := s.setState(NodeStateAlive); changed { - promEVMPoolRPCNodeTransitionsToAlive.WithLabelValues(s.chainID.String(), s.name).Inc() - } - }) - if !ok { - return - } - s.log.Infow("Sendonly RPC Node is online", "nodeState", s.state) - return - } - case <-ctx.Done(): - return - } - } -} diff --git a/core/chains/evm/client/send_only_node_test.go b/core/chains/evm/client/send_only_node_test.go deleted file mode 100644 index 61db09a448c..00000000000 --- a/core/chains/evm/client/send_only_node_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package client_test - -import ( - "fmt" - "math/big" - "net/url" - "testing" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/rpc" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "go.uber.org/zap" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" - "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" -) - -func TestNewSendOnlyNode(t *testing.T) { - t.Parallel() - - urlFormat := "http://user:%s@testurl.com" - password := "pass" - url := testutils.MustParseURL(t, fmt.Sprintf(urlFormat, password)) - redacted := fmt.Sprintf(urlFormat, "xxxxx") - lggr := logger.Test(t) - name := "TestNewSendOnlyNode" - chainID := testutils.NewRandomEVMChainID() - - node := client.NewSendOnlyNode(lggr, *url, name, chainID) - assert.NotNil(t, node) - - // Must contain name & url with redacted password - assert.Contains(t, node.String(), fmt.Sprintf("%s:%s", name, redacted)) - assert.Equal(t, node.ChainID(), chainID) -} - -func TestStartSendOnlyNode(t *testing.T) { - t.Parallel() - - t.Run("Start with Random ChainID", func(t *testing.T) { - t.Parallel() - chainID := testutils.NewRandomEVMChainID() - r := chainIDResp{chainID.Int64(), nil} - url := r.newHTTPServer(t) - lggr, observedLogs := logger.TestObserved(t, zap.WarnLevel) - s := client.NewSendOnlyNode(lggr, *url, t.Name(), chainID) - defer func() { assert.NoError(t, s.Close()) }() - err := s.Start(testutils.Context(t)) - assert.NoError(t, err) // No errors expected - assert.Equal(t, 0, observedLogs.Len()) // No warnings expected - }) - - t.Run("Start with ChainID=0", func(t *testing.T) { - t.Parallel() - lggr, observedLogs := logger.TestObserved(t, zap.WarnLevel) - chainID := testutils.FixtureChainID - r := chainIDResp{chainID.Int64(), nil} - url := r.newHTTPServer(t) - s := client.NewSendOnlyNode(lggr, *url, t.Name(), testutils.FixtureChainID) - - defer func() { assert.NoError(t, s.Close()) }() - err := s.Start(testutils.Context(t)) - assert.NoError(t, err) - // If ChainID = 0, this should get converted into a warning from Start() - testutils.WaitForLogMessage(t, observedLogs, "ChainID verification skipped") - }) - - t.Run("becomes unusable (and remains undialed) if initial dial fails", func(t *testing.T) { - t.Parallel() - lggr, observedLogs := logger.TestObserved(t, zap.WarnLevel) - invalidURL := url.URL{Scheme: "some rubbish", Host: "not a valid host"} - s := client.NewSendOnlyNode(lggr, invalidURL, t.Name(), testutils.FixtureChainID) - - defer func() { assert.NoError(t, s.Close()) }() - err := s.Start(testutils.Context(t)) - require.NoError(t, err) - - assert.False(t, client.IsDialed(s)) - testutils.RequireLogMessage(t, observedLogs, "Dial failed: EVM SendOnly Node is unusable") - }) -} - -func createSignedTx(t *testing.T, chainID *big.Int, nonce uint64, data []byte) *types.Transaction { - key, err := crypto.GenerateKey() - require.NoError(t, err) - sender, err := bind.NewKeyedTransactorWithChainID(key, chainID) - require.NoError(t, err) - tx := cltest.NewLegacyTransaction( - nonce, sender.From, - assets.Ether(100).ToInt(), - 21000, big.NewInt(1000000000), data, - ) - signedTx, err := sender.Signer(sender.From, tx) - require.NoError(t, err) - return signedTx -} - -func TestSendTransaction(t *testing.T) { - t.Parallel() - - chainID := testutils.FixtureChainID - lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) - url := testutils.MustParseURL(t, "http://place.holder") - s := client.NewSendOnlyNode(lggr, - *url, - t.Name(), - testutils.FixtureChainID).(client.TestableSendOnlyNode) - require.NotNil(t, s) - - signedTx := createSignedTx(t, chainID, 1, []byte{1, 2, 3}) - - mockTxSender := mocks.NewTxSender(t) - mockTxSender.On("SendTransaction", mock.Anything, mock.MatchedBy( - func(tx *types.Transaction) bool { - return tx.Nonce() == uint64(1) - }, - )).Once().Return(nil) - s.SetEthClient(nil, mockTxSender) - - err := s.SendTransaction(testutils.Context(t), signedTx) - assert.NoError(t, err) - testutils.WaitForLogMessage(t, observedLogs, "SendOnly RPC call") -} - -func TestBatchCallContext(t *testing.T) { - t.Parallel() - - lggr := logger.Test(t) - chainID := testutils.FixtureChainID - url := testutils.MustParseURL(t, "http://place.holder") - s := client.NewSendOnlyNode( - lggr, - *url, "TestBatchCallContext", - chainID).(client.TestableSendOnlyNode) - - blockNum := hexutil.EncodeBig(big.NewInt(42)) - req := []rpc.BatchElem{ - { - Method: "eth_getBlockByNumber", - Args: []interface{}{blockNum, true}, - Result: &types.Block{}, - }, - { - Method: "method", - Args: []interface{}{1, false}}, - } - - mockBatchSender := mocks.NewBatchSender(t) - mockBatchSender.On("BatchCallContext", mock.Anything, - mock.MatchedBy( - func(b []rpc.BatchElem) bool { - return len(b) == 2 && - b[0].Method == "eth_getBlockByNumber" && b[0].Args[0] == blockNum && b[0].Args[1].(bool) - })).Return(nil).Once().Return(nil) - - s.SetEthClient(mockBatchSender, nil) - - err := s.BatchCallContext(testutils.Context(t), req) - assert.NoError(t, err) -} diff --git a/core/chains/evm/config/chain_scoped.go b/core/chains/evm/config/chain_scoped.go index 17d4120ddf6..ae14c09447b 100644 --- a/core/chains/evm/config/chain_scoped.go +++ b/core/chains/evm/config/chain_scoped.go @@ -68,8 +68,8 @@ func (e *EVMConfig) OCR2() OCR2 { return &ocr2Config{c: e.C.OCR2} } -func (e *EVMConfig) ChainWriter() ChainWriter { - return &chainWriterConfig{c: e.C.ChainWriter} +func (e *EVMConfig) Workflow() Workflow { + return &workflowConfig{c: e.C.Workflow} } func (e *EVMConfig) GasEstimator() GasEstimator { diff --git a/core/chains/evm/config/chain_scoped_ocr2_test.go b/core/chains/evm/config/chain_scoped_ocr2_test.go index 03bf5fd4f14..5a41b4dedac 100644 --- a/core/chains/evm/config/chain_scoped_ocr2_test.go +++ b/core/chains/evm/config/chain_scoped_ocr2_test.go @@ -5,10 +5,10 @@ import ( "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" ) func Test_ocr2Config(t *testing.T) { - evmOcrCfg := cltest.NewTestChainScopedConfig(t) //fallback.toml values - require.Equal(t, uint32(5400000), evmOcrCfg.EVM().OCR2().Automation().GasLimit()) + cfg := testutils.NewTestChainScopedConfig(t, nil) //fallback.toml values + require.Equal(t, uint32(5400000), cfg.EVM().OCR2().Automation().GasLimit()) } diff --git a/core/chains/evm/config/chain_scoped_ocr_test.go b/core/chains/evm/config/chain_scoped_ocr_test.go index e42de465c5c..e027037031c 100644 --- a/core/chains/evm/config/chain_scoped_ocr_test.go +++ b/core/chains/evm/config/chain_scoped_ocr_test.go @@ -2,16 +2,26 @@ package config_test import ( "testing" + "time" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" ) func Test_ocrConfig(t *testing.T) { - evmOcrCfg := cltest.NewTestChainScopedConfig(t) //fallback.toml values - require.Equal(t, uint16(4), evmOcrCfg.EVM().OCR().ContractConfirmations()) - require.Equal(t, cltest.MustParseDuration(t, "10s"), evmOcrCfg.EVM().OCR().ContractTransmitterTransmitTimeout()) - require.Equal(t, cltest.MustParseDuration(t, "10s"), evmOcrCfg.EVM().OCR().DatabaseTimeout()) - require.Equal(t, cltest.MustParseDuration(t, "1s"), evmOcrCfg.EVM().OCR().ObservationGracePeriod()) + cfg := testutils.NewTestChainScopedConfig(t, nil) //fallback.toml values + + require.Equal(t, uint16(4), cfg.EVM().OCR().ContractConfirmations()) + require.Equal(t, mustParseDuration(t, "10s"), cfg.EVM().OCR().ContractTransmitterTransmitTimeout()) + require.Equal(t, mustParseDuration(t, "10s"), cfg.EVM().OCR().DatabaseTimeout()) + require.Equal(t, mustParseDuration(t, "1s"), cfg.EVM().OCR().ObservationGracePeriod()) +} + +func mustParseDuration(t testing.TB, durationStr string) time.Duration { + t.Helper() + + duration, err := time.ParseDuration(durationStr) + require.NoError(t, err) + return duration } diff --git a/core/chains/evm/config/chain_scoped_chain_writer.go b/core/chains/evm/config/chain_scoped_workflow.go similarity index 55% rename from core/chains/evm/config/chain_scoped_chain_writer.go rename to core/chains/evm/config/chain_scoped_workflow.go index 1f1cdcecfa7..36dcb3ea41c 100644 --- a/core/chains/evm/config/chain_scoped_chain_writer.go +++ b/core/chains/evm/config/chain_scoped_workflow.go @@ -5,14 +5,14 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) -type chainWriterConfig struct { - c toml.ChainWriter +type workflowConfig struct { + c toml.Workflow } -func (b *chainWriterConfig) FromAddress() *types.EIP55Address { +func (b *workflowConfig) FromAddress() *types.EIP55Address { return b.c.FromAddress } -func (b *chainWriterConfig) ForwarderAddress() *types.EIP55Address { +func (b *workflowConfig) ForwarderAddress() *types.EIP55Address { return b.c.ForwarderAddress } diff --git a/core/chains/evm/config/config.go b/core/chains/evm/config/config.go index 89e5b6c7d80..e767dad9786 100644 --- a/core/chains/evm/config/config.go +++ b/core/chains/evm/config/config.go @@ -21,7 +21,7 @@ type EVM interface { GasEstimator() GasEstimator OCR() OCR OCR2() OCR2 - ChainWriter() ChainWriter + Workflow() Workflow NodePool() NodePool AutoCreateKey() bool @@ -156,7 +156,7 @@ type BlockHistory interface { TransactionPercentile() uint16 } -type ChainWriter interface { +type Workflow interface { FromAddress() *types.EIP55Address ForwarderAddress() *types.EIP55Address } diff --git a/core/chains/evm/config/config_test.go b/core/chains/evm/config/config_test.go index 69f6ea0875f..26ac2db0852 100644 --- a/core/chains/evm/config/config_test.go +++ b/core/chains/evm/config/config_test.go @@ -1,7 +1,6 @@ package config_test import ( - "fmt" "math/big" "math/rand" "strings" @@ -11,51 +10,27 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - configurl "github.com/smartcontractkit/chainlink-common/pkg/config" - - commonconfig "github.com/smartcontractkit/chainlink/v2/common/config" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" - "github.com/smartcontractkit/chainlink/v2/core/config" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest" - "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" - "github.com/smartcontractkit/chainlink/v2/core/store/models" ) func TestChainScopedConfig(t *testing.T) { t.Parallel() - gcfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - id := ubig.New(big.NewInt(rand.Int63())) - c.EVM[0] = &toml.EVMConfig{ - ChainID: id, - Chain: toml.Defaults(id, &toml.Chain{ - GasEstimator: toml.GasEstimator{PriceMax: assets.NewWeiI(100000000000000)}, - }), - } + cfg := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.GasEstimator.PriceMax = assets.NewWeiI(100000000000000) }) - cfg := evmtest.NewChainScopedConfig(t, gcfg) - - overrides := func(c *chainlink.Config, s *chainlink.Secrets) { - id := ubig.New(big.NewInt(rand.Int63())) - c.EVM[0] = &toml.EVMConfig{ - ChainID: id, - Chain: toml.Defaults(id, &toml.Chain{ - GasEstimator: toml.GasEstimator{ - PriceMax: assets.NewWeiI(100000000000000), - PriceDefault: assets.NewWeiI(42000000000), - }, - }), - } - } + + cfg2 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.GasEstimator.PriceMax = assets.NewWeiI(100000000000000) + c.GasEstimator.PriceDefault = assets.NewWeiI(42000000000) + }) + t.Run("EVM().GasEstimator().PriceDefault()", func(t *testing.T) { assert.Equal(t, assets.NewWeiI(20000000000), cfg.EVM().GasEstimator().PriceDefault()) - gcfg2 := configtest.NewGeneralConfig(t, overrides) - cfg2 := evmtest.NewChainScopedConfig(t, gcfg2) assert.Equal(t, assets.NewWeiI(42000000000), cfg2.EVM().GasEstimator().PriceDefault()) }) @@ -65,53 +40,42 @@ func TestChainScopedConfig(t *testing.T) { }) t.Run("uses customer configured value when set", func(t *testing.T) { - var override uint32 = 10 - gasBumpOverrides := func(c *chainlink.Config, s *chainlink.Secrets) { - id := ubig.New(big.NewInt(rand.Int63())) - c.EVM[0] = &toml.EVMConfig{ - ChainID: id, - Chain: toml.Defaults(id, &toml.Chain{ - GasEstimator: toml.GasEstimator{ - BumpTxDepth: ptr(override), - }, - }), - } - } - gcfg2 := configtest.NewGeneralConfig(t, gasBumpOverrides) - cfg2 := evmtest.NewChainScopedConfig(t, gcfg2) + var bumpTxDepth uint32 = 10 + cfg2 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.GasEstimator.BumpTxDepth = &bumpTxDepth + }) assert.NotEqual(t, cfg2.EVM().Transactions().MaxInFlight(), cfg2.EVM().GasEstimator().BumpTxDepth()) - assert.Equal(t, override, cfg2.EVM().GasEstimator().BumpTxDepth()) + assert.Equal(t, bumpTxDepth, cfg2.EVM().GasEstimator().BumpTxDepth()) }) }) t.Run("PriceMaxKey", func(t *testing.T) { addr := testutils.NewAddress() randomOtherAddr := testutils.NewAddress() - gcfg2 := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - overrides(c, s) - c.EVM[0].KeySpecific = toml.KeySpecificConfig{ + cfg2 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.KeySpecific = toml.KeySpecificConfig{ {Key: ptr(types.EIP55AddressFromAddress(randomOtherAddr)), GasEstimator: toml.KeySpecificGasEstimator{ PriceMax: assets.GWei(850), }, }, } + c.GasEstimator.PriceMax = assets.NewWeiI(100000000000000) + c.GasEstimator.PriceDefault = assets.NewWeiI(42000000000) }) - cfg2 := evmtest.NewChainScopedConfig(t, gcfg2) t.Run("uses chain-specific default value when nothing is set", func(t *testing.T) { assert.Equal(t, assets.NewWeiI(100000000000000), cfg2.EVM().GasEstimator().PriceMaxKey(addr)) }) t.Run("uses chain-specific override value when that is set", func(t *testing.T) { - val := assets.NewWeiI(rand.Int63()) - gcfg3 := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].GasEstimator.PriceMax = val + priceMax := assets.NewWeiI(rand.Int63()) + cfg3 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.GasEstimator.PriceMax = priceMax }) - cfg3 := evmtest.NewChainScopedConfig(t, gcfg3) - - assert.Equal(t, val.String(), cfg3.EVM().GasEstimator().PriceMaxKey(addr).String()) + assert.Equal(t, priceMax.String(), cfg3.EVM().GasEstimator().PriceMaxKey(addr).String()) }) + t.Run("uses key-specific override value when set", func(t *testing.T) { tests := []struct { name string @@ -123,8 +87,8 @@ func TestChainScopedConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gcfg3 := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].KeySpecific = toml.KeySpecificConfig{ + cfg3 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.KeySpecific = toml.KeySpecificConfig{ {Key: ptr(types.EIP55AddressFromAddress(addr)), GasEstimator: toml.KeySpecificGasEstimator{ PriceMax: tt.val, @@ -132,7 +96,6 @@ func TestChainScopedConfig(t *testing.T) { }, } }) - cfg3 := evmtest.NewChainScopedConfig(t, gcfg3) assert.Equal(t, tt.val.String(), cfg3.EVM().GasEstimator().PriceMaxKey(addr).String()) }) @@ -141,9 +104,9 @@ func TestChainScopedConfig(t *testing.T) { t.Run("uses key-specific override value when set and lower than chain specific config", func(t *testing.T) { keySpecificPrice := assets.GWei(900) chainSpecificPrice := assets.GWei(1200) - gcfg3 := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].GasEstimator.PriceMax = chainSpecificPrice - c.EVM[0].KeySpecific = toml.KeySpecificConfig{ + cfg3 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.GasEstimator.PriceMax = chainSpecificPrice + c.KeySpecific = toml.KeySpecificConfig{ {Key: ptr(types.EIP55AddressFromAddress(addr)), GasEstimator: toml.KeySpecificGasEstimator{ PriceMax: keySpecificPrice, @@ -151,16 +114,15 @@ func TestChainScopedConfig(t *testing.T) { }, } }) - cfg3 := evmtest.NewChainScopedConfig(t, gcfg3) assert.Equal(t, keySpecificPrice.String(), cfg3.EVM().GasEstimator().PriceMaxKey(addr).String()) }) t.Run("uses chain-specific value when higher than key-specific value", func(t *testing.T) { keySpecificPrice := assets.GWei(1400) chainSpecificPrice := assets.GWei(1200) - gcfg3 := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].GasEstimator.PriceMax = chainSpecificPrice - c.EVM[0].KeySpecific = toml.KeySpecificConfig{ + cfg3 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.GasEstimator.PriceMax = chainSpecificPrice + c.KeySpecific = toml.KeySpecificConfig{ {Key: ptr(types.EIP55AddressFromAddress(addr)), GasEstimator: toml.KeySpecificGasEstimator{ PriceMax: keySpecificPrice, @@ -168,14 +130,13 @@ func TestChainScopedConfig(t *testing.T) { }, } }) - cfg3 := evmtest.NewChainScopedConfig(t, gcfg3) assert.Equal(t, chainSpecificPrice.String(), cfg3.EVM().GasEstimator().PriceMaxKey(addr).String()) }) t.Run("uses key-specific override value when set and lower than global config", func(t *testing.T) { keySpecificPrice := assets.GWei(900) - gcfg3 := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].KeySpecific = toml.KeySpecificConfig{ + cfg3 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.KeySpecific = toml.KeySpecificConfig{ {Key: ptr(types.EIP55AddressFromAddress(addr)), GasEstimator: toml.KeySpecificGasEstimator{ PriceMax: keySpecificPrice, @@ -183,16 +144,15 @@ func TestChainScopedConfig(t *testing.T) { }, } }) - cfg3 := evmtest.NewChainScopedConfig(t, gcfg3) assert.Equal(t, keySpecificPrice.String(), cfg3.EVM().GasEstimator().PriceMaxKey(addr).String()) }) t.Run("uses global value when higher than key-specific value", func(t *testing.T) { keySpecificPrice := assets.GWei(1400) chainSpecificPrice := assets.GWei(1200) - gcfg3 := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].GasEstimator.PriceMax = chainSpecificPrice - c.EVM[0].KeySpecific = toml.KeySpecificConfig{ + cfg3 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.GasEstimator.PriceMax = chainSpecificPrice + c.KeySpecific = toml.KeySpecificConfig{ {Key: ptr(types.EIP55AddressFromAddress(addr)), GasEstimator: toml.KeySpecificGasEstimator{ PriceMax: keySpecificPrice, @@ -200,19 +160,17 @@ func TestChainScopedConfig(t *testing.T) { }, } }) - cfg3 := evmtest.NewChainScopedConfig(t, gcfg3) assert.Equal(t, chainSpecificPrice.String(), cfg3.EVM().GasEstimator().PriceMaxKey(addr).String()) }) t.Run("uses global value when there is no key-specific price", func(t *testing.T) { - val := assets.NewWeiI(rand.Int63()) + priceMax := assets.NewWeiI(rand.Int63()) unsetAddr := testutils.NewAddress() - gcfg3 := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].GasEstimator.PriceMax = val + cfg3 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.GasEstimator.PriceMax = priceMax }) - cfg3 := evmtest.NewChainScopedConfig(t, gcfg3) - assert.Equal(t, val.String(), cfg3.EVM().GasEstimator().PriceMaxKey(unsetAddr).String()) + assert.Equal(t, priceMax.String(), cfg3.EVM().GasEstimator().PriceMaxKey(unsetAddr).String()) }) }) @@ -222,14 +180,13 @@ func TestChainScopedConfig(t *testing.T) { }) t.Run("uses chain-specific override value when that is set", func(t *testing.T) { - val := testutils.NewAddress() + addr := testutils.NewAddress() - gcfg3 := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].LinkContractAddress = ptr(types.EIP55AddressFromAddress(val)) + cfg3 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.LinkContractAddress = ptr(types.EIP55AddressFromAddress(addr)) }) - cfg3 := evmtest.NewChainScopedConfig(t, gcfg3) - assert.Equal(t, val.String(), cfg3.EVM().LinkContractAddress()) + assert.Equal(t, addr.String(), cfg3.EVM().LinkContractAddress()) }) }) @@ -241,10 +198,9 @@ func TestChainScopedConfig(t *testing.T) { t.Run("uses chain-specific override value when that is set", func(t *testing.T) { val := testutils.NewAddress() - gcfg3 := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].OperatorFactoryAddress = ptr(types.EIP55AddressFromAddress(val)) + cfg3 := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.OperatorFactoryAddress = ptr(types.EIP55AddressFromAddress(val)) }) - cfg3 := evmtest.NewChainScopedConfig(t, gcfg3) assert.Equal(t, val.String(), cfg3.EVM().OperatorFactoryAddress()) }) @@ -253,8 +209,7 @@ func TestChainScopedConfig(t *testing.T) { func TestChainScopedConfig_BlockHistory(t *testing.T) { t.Parallel() - gcfg := configtest.NewTestGeneralConfig(t) - cfg := evmtest.NewChainScopedConfig(t, gcfg) + cfg := testutils.NewTestChainScopedConfig(t, nil) bh := cfg.EVM().GasEstimator().BlockHistory() assert.Equal(t, uint32(25), bh.BatchSize()) @@ -268,10 +223,9 @@ func TestChainScopedConfig_BlockHistory(t *testing.T) { func TestChainScopedConfig_GasEstimator(t *testing.T) { t.Parallel() - gcfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - c.EVM[0].GasEstimator.PriceMax = assets.GWei(500) + cfg := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.GasEstimator.PriceMax = assets.GWei(500) }) - cfg := evmtest.NewChainScopedConfig(t, gcfg) ge := cfg.EVM().GasEstimator() assert.Equal(t, "BlockHistory", ge.Mode()) @@ -292,17 +246,9 @@ func TestChainScopedConfig_GasEstimator(t *testing.T) { } func TestChainScopedConfig_BSCDefaults(t *testing.T) { - chainID := big.NewInt(56) - gcfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, secrets *chainlink.Secrets) { - id := ubig.New(chainID) - cfg := toml.Defaults(id) - c.EVM[0] = &toml.EVMConfig{ - ChainID: id, - Enabled: ptr(true), - Chain: cfg, - } + cfg := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.ChainID = (*ubig.Big)(big.NewInt(56)) }) - cfg := evmtest.NewChainScopedConfig(t, gcfg) timeout := cfg.EVM().OCR().DatabaseTimeout() require.Equal(t, 2*time.Second, timeout) @@ -345,16 +291,9 @@ func TestChainScopedConfig_Profiles(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - gcfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, secrets *chainlink.Secrets) { - id := ubig.NewI(tt.chainID) - cfg := toml.Defaults(id) - c.EVM[0] = &toml.EVMConfig{ - ChainID: id, - Enabled: ptr(true), - Chain: cfg, - } + config := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { + c.ChainID = ubig.NewI(tt.chainID) }) - config := evmtest.NewChainScopedConfig(t, gcfg) assert.Equal(t, tt.expectedGasLimitDefault, config.EVM().GasEstimator().LimitDefault()) assert.Nil(t, config.EVM().GasEstimator().LimitJobType().OCR()) @@ -369,8 +308,7 @@ func TestChainScopedConfig_Profiles(t *testing.T) { func TestChainScopedConfig_HeadTracker(t *testing.T) { t.Parallel() - gcfg := configtest.NewTestGeneralConfig(t) - cfg := evmtest.NewChainScopedConfig(t, gcfg) + cfg := testutils.NewTestChainScopedConfig(t, nil) ht := cfg.EVM().HeadTracker() assert.Equal(t, uint32(100), ht.HistoryDepth()) @@ -380,100 +318,8 @@ func TestChainScopedConfig_HeadTracker(t *testing.T) { assert.Equal(t, uint32(10000), ht.MaxAllowedFinalityDepth()) } -func Test_chainScopedConfig_Validate(t *testing.T) { - configWithChains := func(t *testing.T, id int64, chains ...*toml.Chain) config.AppConfig { - return configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - s.Database.URL = models.MustSecretURL("postgresql://doesnotexist:justtopassvalidationtests@localhost:5432/chainlink_na_test") - chainID := ubig.NewI(id) - c.EVM[0] = &toml.EVMConfig{ChainID: chainID, Enabled: ptr(true), Chain: toml.Defaults(chainID, chains...), - Nodes: toml.EVMNodes{{ - Name: ptr("fake"), - WSURL: configurl.MustParseURL("wss://foo.test/ws"), - HTTPURL: configurl.MustParseURL("http://foo.test"), - }}} - }) - } - - // Validate built-in - for _, id := range toml.DefaultIDs { - id := id - t.Run(fmt.Sprintf("chainID-%s", id), func(t *testing.T) { - cfg := configWithChains(t, id.Int64()) - assert.NoError(t, cfg.Validate()) - }) - } - - // Invalid Cases: - - t.Run("arbitrum-estimator", func(t *testing.T) { - t.Run("custom", func(t *testing.T) { - cfg := configWithChains(t, 0, &toml.Chain{ - ChainType: commonconfig.NewChainTypeConfig(string(commonconfig.ChainArbitrum)), - GasEstimator: toml.GasEstimator{ - Mode: ptr("BlockHistory"), - }, - }) - assert.NoError(t, cfg.Validate()) - }) - t.Run("mainnet", func(t *testing.T) { - cfg := configWithChains(t, 42161, &toml.Chain{ - GasEstimator: toml.GasEstimator{ - Mode: ptr("BlockHistory"), - BlockHistory: toml.BlockHistoryEstimator{ - BlockHistorySize: ptr[uint16](1), - }, - }, - }) - assert.NoError(t, cfg.Validate()) - }) - t.Run("testnet", func(t *testing.T) { - cfg := configWithChains(t, 421611, &toml.Chain{ - GasEstimator: toml.GasEstimator{ - Mode: ptr("SuggestedPrice"), - }, - }) - assert.NoError(t, cfg.Validate()) - }) - }) - - t.Run("optimism-estimator", func(t *testing.T) { - t.Run("custom", func(t *testing.T) { - cfg := configWithChains(t, 0, &toml.Chain{ - ChainType: commonconfig.NewChainTypeConfig(string(commonconfig.ChainOptimismBedrock)), - GasEstimator: toml.GasEstimator{ - Mode: ptr("BlockHistory"), - }, - }) - assert.NoError(t, cfg.Validate()) - }) - t.Run("mainnet", func(t *testing.T) { - cfg := configWithChains(t, 10, &toml.Chain{ - GasEstimator: toml.GasEstimator{ - Mode: ptr("FixedPrice"), - }, - }) - assert.NoError(t, cfg.Validate()) - }) - t.Run("testnet", func(t *testing.T) { - cfg := configWithChains(t, 69, &toml.Chain{ - GasEstimator: toml.GasEstimator{ - Mode: ptr("FixedPrice"), - }, - }) - assert.NoError(t, cfg.Validate()) - }) - }) -} - func TestNodePoolConfig(t *testing.T) { - gcfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - id := ubig.New(big.NewInt(rand.Int63())) - c.EVM[0] = &toml.EVMConfig{ - ChainID: id, - Chain: toml.Defaults(id, &toml.Chain{}), - } - }) - cfg := evmtest.NewChainScopedConfig(t, gcfg) + cfg := testutils.NewTestChainScopedConfig(t, nil) require.Equal(t, "HighestHead", cfg.EVM().NodePool().SelectionMode()) require.Equal(t, uint32(5), cfg.EVM().NodePool().SyncThreshold()) @@ -486,37 +332,28 @@ func TestClientErrorsConfig(t *testing.T) { t.Parallel() t.Run("EVM().NodePool().Errors()", func(t *testing.T) { - clientErrorsOverrides := func(c *chainlink.Config, s *chainlink.Secrets) { + cfg := testutils.NewTestChainScopedConfig(t, func(c *toml.EVMConfig) { id := ubig.New(big.NewInt(rand.Int63())) - c.EVM[0] = &toml.EVMConfig{ - ChainID: id, - Chain: toml.Defaults(id, &toml.Chain{ - NodePool: toml.NodePool{ - Errors: toml.ClientErrors{ - NonceTooLow: ptr[string]("client error nonce too low"), - NonceTooHigh: ptr[string]("client error nonce too high"), - ReplacementTransactionUnderpriced: ptr[string]("client error replacement underpriced"), - LimitReached: ptr[string]("client error limit reached"), - TransactionAlreadyInMempool: ptr[string]("client error transaction already in mempool"), - TerminallyUnderpriced: ptr[string]("client error terminally underpriced"), - InsufficientEth: ptr[string]("client error insufficient eth"), - TxFeeExceedsCap: ptr[string]("client error tx fee exceeds cap"), - L2FeeTooLow: ptr[string]("client error l2 fee too low"), - L2FeeTooHigh: ptr[string]("client error l2 fee too high"), - L2Full: ptr[string]("client error l2 full"), - TransactionAlreadyMined: ptr[string]("client error transaction already mined"), - Fatal: ptr[string]("client error fatal"), - ServiceUnavailable: ptr[string]("client error service unavailable"), - }, - }, - }), + c.ChainID = id + c.NodePool = toml.NodePool{ + Errors: toml.ClientErrors{ + NonceTooLow: ptr("client error nonce too low"), + NonceTooHigh: ptr("client error nonce too high"), + ReplacementTransactionUnderpriced: ptr("client error replacement underpriced"), + LimitReached: ptr("client error limit reached"), + TransactionAlreadyInMempool: ptr("client error transaction already in mempool"), + TerminallyUnderpriced: ptr("client error terminally underpriced"), + InsufficientEth: ptr("client error insufficient eth"), + TxFeeExceedsCap: ptr("client error tx fee exceeds cap"), + L2FeeTooLow: ptr("client error l2 fee too low"), + L2FeeTooHigh: ptr("client error l2 fee too high"), + L2Full: ptr("client error l2 full"), + TransactionAlreadyMined: ptr("client error transaction already mined"), + Fatal: ptr("client error fatal"), + ServiceUnavailable: ptr("client error service unavailable"), + }, } - } - - gcfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - clientErrorsOverrides(c, s) }) - cfg := evmtest.NewChainScopedConfig(t, gcfg) errors := cfg.EVM().NodePool().Errors() assert.Equal(t, "client error nonce too low", errors.NonceTooLow()) diff --git a/core/chains/evm/config/toml/config.go b/core/chains/evm/config/toml/config.go index 265557ae9b1..38385c47c27 100644 --- a/core/chains/evm/config/toml/config.go +++ b/core/chains/evm/config/toml/config.go @@ -367,7 +367,7 @@ type Chain struct { NodePool NodePool `toml:",omitempty"` OCR OCR `toml:",omitempty"` OCR2 OCR2 `toml:",omitempty"` - ChainWriter ChainWriter `toml:",omitempty"` + Workflow Workflow `toml:",omitempty"` } func (c *Chain) ValidateConfig() (err error) { @@ -507,12 +507,12 @@ func (a *Automation) setFrom(f *Automation) { } } -type ChainWriter struct { +type Workflow struct { FromAddress *types.EIP55Address `toml:",omitempty"` ForwarderAddress *types.EIP55Address `toml:",omitempty"` } -func (m *ChainWriter) setFrom(f *ChainWriter) { +func (m *Workflow) setFrom(f *Workflow) { if v := f.FromAddress; v != nil { m.FromAddress = v } diff --git a/core/chains/evm/config/toml/config_test.go b/core/chains/evm/config/toml/config_test.go new file mode 100644 index 00000000000..61b75e229a1 --- /dev/null +++ b/core/chains/evm/config/toml/config_test.go @@ -0,0 +1,31 @@ +package toml_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-common/pkg/config" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" +) + +func TestEVMConfig_ValidateConfig(t *testing.T) { + name := "fake" + for _, id := range toml.DefaultIDs { + t.Run(fmt.Sprintf("chainID-%s", id), func(t *testing.T) { + evmCfg := &toml.EVMConfig{ + ChainID: id, + Chain: toml.Defaults(id), + Nodes: toml.EVMNodes{{ + Name: &name, + WSURL: config.MustParseURL("wss://foo.test/ws"), + HTTPURL: config.MustParseURL("http://foo.test"), + }}, + } + + assert.NoError(t, config.Validate(evmCfg)) + }) + } +} diff --git a/core/chains/evm/config/toml/defaults.go b/core/chains/evm/config/toml/defaults.go index 622ac132e13..6e60454f3eb 100644 --- a/core/chains/evm/config/toml/defaults.go +++ b/core/chains/evm/config/toml/defaults.go @@ -181,5 +181,5 @@ func (c *Chain) SetFrom(f *Chain) { c.NodePool.setFrom(&f.NodePool) c.OCR.setFrom(&f.OCR) c.OCR2.setFrom(&f.OCR2) - c.ChainWriter.setFrom(&f.ChainWriter) + c.Workflow.setFrom(&f.Workflow) } diff --git a/core/chains/evm/config/toml/defaults/Avalanche_Fuji.toml b/core/chains/evm/config/toml/defaults/Avalanche_Fuji.toml index 37d2a9d0777..d7cbad8157c 100644 --- a/core/chains/evm/config/toml/defaults/Avalanche_Fuji.toml +++ b/core/chains/evm/config/toml/defaults/Avalanche_Fuji.toml @@ -1,5 +1,5 @@ ChainID = '43113' -FinalityDepth = 1 +FinalityDepth = 10 LinkContractAddress = '0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846' LogPollInterval = '3s' MinIncomingConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Avalanche_Mainnet.toml b/core/chains/evm/config/toml/defaults/Avalanche_Mainnet.toml index 5e2d8dd4274..95d4bf75460 100644 --- a/core/chains/evm/config/toml/defaults/Avalanche_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Avalanche_Mainnet.toml @@ -1,5 +1,5 @@ ChainID = '43114' -FinalityDepth = 1 +FinalityDepth = 10 LinkContractAddress = '0x5947BB275c521040051D82396192181b413227A3' LogPollInterval = '3s' MinIncomingConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Celo_Mainnet.toml b/core/chains/evm/config/toml/defaults/Celo_Mainnet.toml index 87c6384083b..b48cb25b325 100644 --- a/core/chains/evm/config/toml/defaults/Celo_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Celo_Mainnet.toml @@ -1,6 +1,6 @@ ChainID = '42220' ChainType = 'celo' -FinalityDepth = 1 +FinalityDepth = 10 LogPollInterval = '5s' MinIncomingConfirmations = 1 NoNewHeadsThreshold = '1m' diff --git a/core/chains/evm/config/toml/defaults/Celo_Testnet.toml b/core/chains/evm/config/toml/defaults/Celo_Testnet.toml index 0508e86da3a..d3f595baac6 100644 --- a/core/chains/evm/config/toml/defaults/Celo_Testnet.toml +++ b/core/chains/evm/config/toml/defaults/Celo_Testnet.toml @@ -1,6 +1,6 @@ ChainID = '44787' ChainType = 'celo' -FinalityDepth = 1 +FinalityDepth = 10 LogPollInterval = '5s' MinIncomingConfirmations = 1 NoNewHeadsThreshold = '1m' diff --git a/core/chains/evm/config/toml/defaults/Ethereum_Kovan.toml b/core/chains/evm/config/toml/defaults/Ethereum_Kovan.toml index d3361bb373f..0a4a351ade9 100644 --- a/core/chains/evm/config/toml/defaults/Ethereum_Kovan.toml +++ b/core/chains/evm/config/toml/defaults/Ethereum_Kovan.toml @@ -5,7 +5,7 @@ OperatorFactoryAddress = '0x8007e24251b1D2Fc518Eb843A701d9cD21fe0aA3' [GasEstimator] # FIXME: Kovan has strange behaviour with EIP1559, see: -# https://app.shortcut.com/chainlinklabs/story/34098/kovan-can-emit-blocks-that-violate-assumptions-in-block-history-estimator +# https://smartcontract-it.atlassian.net/browse/BCF-1420 EIP1559DynamicFees = false [GasEstimator.BlockHistory] diff --git a/core/chains/evm/config/toml/defaults/Ethereum_Rinkeby.toml b/core/chains/evm/config/toml/defaults/Ethereum_Rinkeby.toml index 75b01bfb03d..06dc2cb0f58 100644 --- a/core/chains/evm/config/toml/defaults/Ethereum_Rinkeby.toml +++ b/core/chains/evm/config/toml/defaults/Ethereum_Rinkeby.toml @@ -4,7 +4,7 @@ MinContractPayment = '0.1 link' [GasEstimator] # TODO: EIP1559 on rinkeby has not been adequately tested, see: -# https://app.shortcut.com/chainlinklabs/story/34098/kovan-can-emit-blocks-that-violate-assumptions-in-block-history-estimator +# https://smartcontract-it.atlassian.net/browse/BCF-1420 EIP1559DynamicFees = false [GasEstimator.BlockHistory] diff --git a/core/chains/evm/config/toml/defaults/Klaytn_Mainnet.toml b/core/chains/evm/config/toml/defaults/Klaytn_Mainnet.toml index 1994f309afc..7cf7c2d62e3 100644 --- a/core/chains/evm/config/toml/defaults/Klaytn_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Klaytn_Mainnet.toml @@ -1,5 +1,5 @@ ChainID = '8217' -FinalityDepth = 1 +FinalityDepth = 10 MinIncomingConfirmations = 1 NoNewHeadsThreshold = '30s' OCR.ContractConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Klaytn_Testnet.toml b/core/chains/evm/config/toml/defaults/Klaytn_Testnet.toml index 625afd649ea..a938d46c5cd 100644 --- a/core/chains/evm/config/toml/defaults/Klaytn_Testnet.toml +++ b/core/chains/evm/config/toml/defaults/Klaytn_Testnet.toml @@ -1,5 +1,5 @@ ChainID = '1001' -FinalityDepth = 1 +FinalityDepth = 10 MinIncomingConfirmations = 1 NoNewHeadsThreshold = '30s' OCR.ContractConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml b/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml index f67b6ecce0a..cfc79f978f5 100644 --- a/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Metis_Mainnet.toml @@ -2,7 +2,7 @@ ChainID = '1088' ChainType = 'metis' # Sequencer offers absolute finality -FinalityDepth = 1 +FinalityDepth = 10 MinIncomingConfirmations = 1 NoNewHeadsThreshold = '0' OCR.ContractConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Metis_Rinkeby.toml b/core/chains/evm/config/toml/defaults/Metis_Rinkeby.toml index 637dbb41203..e481282abf1 100644 --- a/core/chains/evm/config/toml/defaults/Metis_Rinkeby.toml +++ b/core/chains/evm/config/toml/defaults/Metis_Rinkeby.toml @@ -1,6 +1,6 @@ ChainID = '588' ChainType = 'metis' -FinalityDepth = 1 +FinalityDepth = 10 MinIncomingConfirmations = 1 NoNewHeadsThreshold = '0' OCR.ContractConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml b/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml index 9354b7a7f04..4e6307302f5 100644 --- a/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml +++ b/core/chains/evm/config/toml/defaults/Metis_Sepolia.toml @@ -1,6 +1,6 @@ ChainID = '59902' ChainType = 'metis' -FinalityDepth = 1 +FinalityDepth = 10 MinIncomingConfirmations = 1 NoNewHeadsThreshold = '0' OCR.ContractConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Scroll_Mainnet.toml b/core/chains/evm/config/toml/defaults/Scroll_Mainnet.toml index 21d4699ee8a..47895e6a6d5 100644 --- a/core/chains/evm/config/toml/defaults/Scroll_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/Scroll_Mainnet.toml @@ -1,5 +1,5 @@ ChainID = '534352' -FinalityDepth = 1 +FinalityDepth = 10 ChainType = 'scroll' LogPollInterval = '3s' MinIncomingConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Scroll_Sepolia.toml b/core/chains/evm/config/toml/defaults/Scroll_Sepolia.toml index 27ac984865d..d2626a86bd4 100644 --- a/core/chains/evm/config/toml/defaults/Scroll_Sepolia.toml +++ b/core/chains/evm/config/toml/defaults/Scroll_Sepolia.toml @@ -1,5 +1,5 @@ ChainID = '534351' -FinalityDepth = 1 +FinalityDepth = 10 ChainType = 'scroll' LogPollInterval = '3s' MinIncomingConfirmations = 1 diff --git a/core/chains/evm/config/toml/defaults/Simulated.toml b/core/chains/evm/config/toml/defaults/Simulated.toml index 8dc71a33edd..52e78c94edf 100644 --- a/core/chains/evm/config/toml/defaults/Simulated.toml +++ b/core/chains/evm/config/toml/defaults/Simulated.toml @@ -1,5 +1,5 @@ ChainID = '1337' -FinalityDepth = 1 +FinalityDepth = 10 MinIncomingConfirmations = 1 MinContractPayment = '100' NoNewHeadsThreshold = '0s' diff --git a/core/chains/evm/config/toml/defaults/WeMix_Mainnet.toml b/core/chains/evm/config/toml/defaults/WeMix_Mainnet.toml index ee50a9844a4..35cd4a90a2f 100644 --- a/core/chains/evm/config/toml/defaults/WeMix_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/WeMix_Mainnet.toml @@ -1,6 +1,6 @@ ChainID = '1111' ChainType = 'wemix' -FinalityDepth = 1 +FinalityDepth = 10 MinIncomingConfirmations = 1 # WeMix emits a block every 1 second, regardless of transactions LogPollInterval = '3s' diff --git a/core/chains/evm/config/toml/defaults/WeMix_Testnet.toml b/core/chains/evm/config/toml/defaults/WeMix_Testnet.toml index c089668f8ba..417718d87eb 100644 --- a/core/chains/evm/config/toml/defaults/WeMix_Testnet.toml +++ b/core/chains/evm/config/toml/defaults/WeMix_Testnet.toml @@ -1,6 +1,6 @@ ChainID = '1112' ChainType = 'wemix' -FinalityDepth = 1 +FinalityDepth = 10 MinIncomingConfirmations = 1 # WeMix emits a block every 1 second, regardless of transactions LogPollInterval = '3s' diff --git a/core/chains/evm/config/toml/defaults/zkSync_Goerli.toml b/core/chains/evm/config/toml/defaults/zkSync_Goerli.toml index be3bc22b812..05d85cfe248 100644 --- a/core/chains/evm/config/toml/defaults/zkSync_Goerli.toml +++ b/core/chains/evm/config/toml/defaults/zkSync_Goerli.toml @@ -1,6 +1,6 @@ ChainID = '280' ChainType = 'zksync' -FinalityDepth = 1 +FinalityDepth = 10 LogPollInterval = '5s' MinIncomingConfirmations = 1 NoNewHeadsThreshold = '1m' @@ -11,4 +11,4 @@ PriceMax = 18446744073709551615 PriceMin = 0 [HeadTracker] -HistoryDepth = 5 +HistoryDepth = 50 diff --git a/core/chains/evm/config/toml/defaults/zkSync_Mainnet.toml b/core/chains/evm/config/toml/defaults/zkSync_Mainnet.toml index 84ddac91db3..02ce2b0f18c 100644 --- a/core/chains/evm/config/toml/defaults/zkSync_Mainnet.toml +++ b/core/chains/evm/config/toml/defaults/zkSync_Mainnet.toml @@ -1,6 +1,6 @@ ChainID = '324' ChainType = 'zksync' -FinalityDepth = 1 +FinalityDepth = 10 LogPollInterval = '5s' MinIncomingConfirmations = 1 NoNewHeadsThreshold = '1m' @@ -11,4 +11,4 @@ PriceMax = 18446744073709551615 PriceMin = 0 [HeadTracker] -HistoryDepth = 5 +HistoryDepth = 50 diff --git a/core/chains/evm/config/toml/defaults/zkSync_Sepolia.toml b/core/chains/evm/config/toml/defaults/zkSync_Sepolia.toml index 0801e54acc9..b4e957a6c99 100644 --- a/core/chains/evm/config/toml/defaults/zkSync_Sepolia.toml +++ b/core/chains/evm/config/toml/defaults/zkSync_Sepolia.toml @@ -1,6 +1,6 @@ ChainID = '300' ChainType = 'zksync' -FinalityDepth = 1 +FinalityDepth = 10 LogPollInterval = '5s' MinIncomingConfirmations = 1 NoNewHeadsThreshold = '1m' @@ -11,4 +11,4 @@ PriceMax = 18446744073709551615 PriceMin = 0 [HeadTracker] -HistoryDepth = 5 +HistoryDepth = 50 diff --git a/core/chains/evm/forwarders/forwarder_manager.go b/core/chains/evm/forwarders/forwarder_manager.go index 39ca8236305..638836094dc 100644 --- a/core/chains/evm/forwarders/forwarder_manager.go +++ b/core/chains/evm/forwarders/forwarder_manager.go @@ -43,7 +43,7 @@ type FwdMgr struct { logpoller evmlogpoller.LogPoller // TODO(samhassan): sendersCache should be an LRU capped cache - // https://app.shortcut.com/chainlinklabs/story/37884/forwarder-manager-uses-lru-for-caching-dest-addresses + // https://smartcontract-it.atlassian.net/browse/ARCHIVE-22505 sendersCache map[common.Address][]common.Address latestBlock int64 diff --git a/core/chains/evm/mocks/node.go b/core/chains/evm/mocks/node.go deleted file mode 100644 index 6a939d5e844..00000000000 --- a/core/chains/evm/mocks/node.go +++ /dev/null @@ -1,881 +0,0 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. - -package mocks - -import ( - big "math/big" - - common "github.com/ethereum/go-ethereum/common" - client "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - - context "context" - - ethereum "github.com/ethereum/go-ethereum" - - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - - mock "github.com/stretchr/testify/mock" - - rpc "github.com/ethereum/go-ethereum/rpc" - - types "github.com/ethereum/go-ethereum/core/types" -) - -// Node is an autogenerated mock type for the Node type -type Node struct { - mock.Mock -} - -// BalanceAt provides a mock function with given fields: ctx, account, blockNumber -func (_m *Node) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { - ret := _m.Called(ctx, account, blockNumber) - - if len(ret) == 0 { - panic("no return value specified for BalanceAt") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) (*big.Int, error)); ok { - return rf(ctx, account, blockNumber) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) *big.Int); ok { - r0 = rf(ctx, account, blockNumber) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address, *big.Int) error); ok { - r1 = rf(ctx, account, blockNumber) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BatchCallContext provides a mock function with given fields: ctx, b -func (_m *Node) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - ret := _m.Called(ctx, b) - - if len(ret) == 0 { - panic("no return value specified for BatchCallContext") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, []rpc.BatchElem) error); ok { - r0 = rf(ctx, b) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// BlockByHash provides a mock function with given fields: ctx, hash -func (_m *Node) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - ret := _m.Called(ctx, hash) - - if len(ret) == 0 { - panic("no return value specified for BlockByHash") - } - - var r0 *types.Block - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Block, error)); ok { - return rf(ctx, hash) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Block); ok { - r0 = rf(ctx, hash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Block) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(ctx, hash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockByNumber provides a mock function with given fields: ctx, number -func (_m *Node) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - ret := _m.Called(ctx, number) - - if len(ret) == 0 { - panic("no return value specified for BlockByNumber") - } - - var r0 *types.Block - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Block, error)); ok { - return rf(ctx, number) - } - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *types.Block); ok { - r0 = rf(ctx, number) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Block) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { - r1 = rf(ctx, number) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockNumber provides a mock function with given fields: ctx -func (_m *Node) BlockNumber(ctx context.Context) (uint64, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for BlockNumber") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// CallContext provides a mock function with given fields: ctx, result, method, args -func (_m *Node) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { - var _ca []interface{} - _ca = append(_ca, ctx, result, method) - _ca = append(_ca, args...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for CallContext") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, interface{}, string, ...interface{}) error); ok { - r0 = rf(ctx, result, method, args...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// CallContract provides a mock function with given fields: ctx, msg, blockNumber -func (_m *Node) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - ret := _m.Called(ctx, msg, blockNumber) - - if len(ret) == 0 { - panic("no return value specified for CallContract") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error)); ok { - return rf(ctx, msg, blockNumber) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg, *big.Int) []byte); ok { - r0 = rf(ctx, msg, blockNumber) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg, *big.Int) error); ok { - r1 = rf(ctx, msg, blockNumber) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ChainID provides a mock function with given fields: -func (_m *Node) ChainID() *big.Int { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for ChainID") - } - - var r0 *big.Int - if rf, ok := ret.Get(0).(func() *big.Int); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - return r0 -} - -// Close provides a mock function with given fields: -func (_m *Node) Close() error { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Close") - } - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// CodeAt provides a mock function with given fields: ctx, account, blockNumber -func (_m *Node) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { - ret := _m.Called(ctx, account, blockNumber) - - if len(ret) == 0 { - panic("no return value specified for CodeAt") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) ([]byte, error)); ok { - return rf(ctx, account, blockNumber) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) []byte); ok { - r0 = rf(ctx, account, blockNumber) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address, *big.Int) error); ok { - r1 = rf(ctx, account, blockNumber) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// EstimateGas provides a mock function with given fields: ctx, call -func (_m *Node) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { - ret := _m.Called(ctx, call) - - if len(ret) == 0 { - panic("no return value specified for EstimateGas") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) (uint64, error)); ok { - return rf(ctx, call) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) uint64); ok { - r0 = rf(ctx, call) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg) error); ok { - r1 = rf(ctx, call) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// EthSubscribe provides a mock function with given fields: ctx, channel, args -func (_m *Node) EthSubscribe(ctx context.Context, channel chan<- *evmtypes.Head, args ...interface{}) (ethereum.Subscription, error) { - var _ca []interface{} - _ca = append(_ca, ctx, channel) - _ca = append(_ca, args...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for EthSubscribe") - } - - var r0 ethereum.Subscription - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, chan<- *evmtypes.Head, ...interface{}) (ethereum.Subscription, error)); ok { - return rf(ctx, channel, args...) - } - if rf, ok := ret.Get(0).(func(context.Context, chan<- *evmtypes.Head, ...interface{}) ethereum.Subscription); ok { - r0 = rf(ctx, channel, args...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ethereum.Subscription) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, chan<- *evmtypes.Head, ...interface{}) error); ok { - r1 = rf(ctx, channel, args...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// FilterLogs provides a mock function with given fields: ctx, q -func (_m *Node) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { - ret := _m.Called(ctx, q) - - if len(ret) == 0 { - panic("no return value specified for FilterLogs") - } - - var r0 []types.Log - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery) ([]types.Log, error)); ok { - return rf(ctx, q) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery) []types.Log); ok { - r0 = rf(ctx, q) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]types.Log) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.FilterQuery) error); ok { - r1 = rf(ctx, q) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// HeaderByHash provides a mock function with given fields: _a0, _a1 -func (_m *Node) HeaderByHash(_a0 context.Context, _a1 common.Hash) (*types.Header, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for HeaderByHash") - } - - var r0 *types.Header - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Header, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Header); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Header) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// HeaderByNumber provides a mock function with given fields: _a0, _a1 -func (_m *Node) HeaderByNumber(_a0 context.Context, _a1 *big.Int) (*types.Header, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for HeaderByNumber") - } - - var r0 *types.Header - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Header, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *types.Header); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Header) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Name provides a mock function with given fields: -func (_m *Node) Name() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Name") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// NonceAt provides a mock function with given fields: ctx, account, blockNumber -func (_m *Node) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { - ret := _m.Called(ctx, account, blockNumber) - - if len(ret) == 0 { - panic("no return value specified for NonceAt") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) (uint64, error)); ok { - return rf(ctx, account, blockNumber) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) uint64); ok { - r0 = rf(ctx, account, blockNumber) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address, *big.Int) error); ok { - r1 = rf(ctx, account, blockNumber) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Order provides a mock function with given fields: -func (_m *Node) Order() int32 { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Order") - } - - var r0 int32 - if rf, ok := ret.Get(0).(func() int32); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(int32) - } - - return r0 -} - -// PendingCallContract provides a mock function with given fields: ctx, msg -func (_m *Node) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { - ret := _m.Called(ctx, msg) - - if len(ret) == 0 { - panic("no return value specified for PendingCallContract") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) ([]byte, error)); ok { - return rf(ctx, msg) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) []byte); ok { - r0 = rf(ctx, msg) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg) error); ok { - r1 = rf(ctx, msg) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// PendingCodeAt provides a mock function with given fields: ctx, account -func (_m *Node) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { - ret := _m.Called(ctx, account) - - if len(ret) == 0 { - panic("no return value specified for PendingCodeAt") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address) ([]byte, error)); ok { - return rf(ctx, account) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address) []byte); ok { - r0 = rf(ctx, account) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address) error); ok { - r1 = rf(ctx, account) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// PendingNonceAt provides a mock function with given fields: ctx, account -func (_m *Node) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { - ret := _m.Called(ctx, account) - - if len(ret) == 0 { - panic("no return value specified for PendingNonceAt") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Address) (uint64, error)); ok { - return rf(ctx, account) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Address) uint64); ok { - r0 = rf(ctx, account) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Address) error); ok { - r1 = rf(ctx, account) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SendTransaction provides a mock function with given fields: ctx, tx -func (_m *Node) SendTransaction(ctx context.Context, tx *types.Transaction) error { - ret := _m.Called(ctx, tx) - - if len(ret) == 0 { - panic("no return value specified for SendTransaction") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) error); ok { - r0 = rf(ctx, tx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Start provides a mock function with given fields: ctx -func (_m *Node) Start(ctx context.Context) error { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for Start") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(ctx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// State provides a mock function with given fields: -func (_m *Node) State() client.NodeState { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for State") - } - - var r0 client.NodeState - if rf, ok := ret.Get(0).(func() client.NodeState); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(client.NodeState) - } - - return r0 -} - -// StateAndLatest provides a mock function with given fields: -func (_m *Node) StateAndLatest() (client.NodeState, int64, *big.Int) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for StateAndLatest") - } - - var r0 client.NodeState - var r1 int64 - var r2 *big.Int - if rf, ok := ret.Get(0).(func() (client.NodeState, int64, *big.Int)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() client.NodeState); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(client.NodeState) - } - - if rf, ok := ret.Get(1).(func() int64); ok { - r1 = rf() - } else { - r1 = ret.Get(1).(int64) - } - - if rf, ok := ret.Get(2).(func() *big.Int); ok { - r2 = rf() - } else { - if ret.Get(2) != nil { - r2 = ret.Get(2).(*big.Int) - } - } - - return r0, r1, r2 -} - -// String provides a mock function with given fields: -func (_m *Node) String() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for String") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// SubscribeFilterLogs provides a mock function with given fields: ctx, q, ch -func (_m *Node) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - ret := _m.Called(ctx, q, ch) - - if len(ret) == 0 { - panic("no return value specified for SubscribeFilterLogs") - } - - var r0 ethereum.Subscription - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery, chan<- types.Log) (ethereum.Subscription, error)); ok { - return rf(ctx, q, ch) - } - if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery, chan<- types.Log) ethereum.Subscription); ok { - r0 = rf(ctx, q, ch) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ethereum.Subscription) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ethereum.FilterQuery, chan<- types.Log) error); ok { - r1 = rf(ctx, q, ch) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SubscribersCount provides a mock function with given fields: -func (_m *Node) SubscribersCount() int32 { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for SubscribersCount") - } - - var r0 int32 - if rf, ok := ret.Get(0).(func() int32); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(int32) - } - - return r0 -} - -// SuggestGasPrice provides a mock function with given fields: ctx -func (_m *Node) SuggestGasPrice(ctx context.Context) (*big.Int, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for SuggestGasPrice") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SuggestGasTipCap provides a mock function with given fields: ctx -func (_m *Node) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for SuggestGasTipCap") - } - - var r0 *big.Int - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// TransactionByHash provides a mock function with given fields: ctx, txHash -func (_m *Node) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, error) { - ret := _m.Called(ctx, txHash) - - if len(ret) == 0 { - panic("no return value specified for TransactionByHash") - } - - var r0 *types.Transaction - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Transaction, error)); ok { - return rf(ctx, txHash) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Transaction); ok { - r0 = rf(ctx, txHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Transaction) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(ctx, txHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// TransactionReceipt provides a mock function with given fields: ctx, txHash -func (_m *Node) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { - ret := _m.Called(ctx, txHash) - - if len(ret) == 0 { - panic("no return value specified for TransactionReceipt") - } - - var r0 *types.Receipt - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Receipt, error)); ok { - return rf(ctx, txHash) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Receipt); ok { - r0 = rf(ctx, txHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Receipt) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(ctx, txHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// UnsubscribeAllExceptAliveLoop provides a mock function with given fields: -func (_m *Node) UnsubscribeAllExceptAliveLoop() { - _m.Called() -} - -// NewNode creates a new instance of Node. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewNode(t interface { - mock.TestingT - Cleanup(func()) -}) *Node { - mock := &Node{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/core/chains/evm/mocks/send_only_node.go b/core/chains/evm/mocks/send_only_node.go deleted file mode 100644 index e0ab9775be9..00000000000 --- a/core/chains/evm/mocks/send_only_node.go +++ /dev/null @@ -1,181 +0,0 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. - -package mocks - -import ( - context "context" - big "math/big" - - client "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - - mock "github.com/stretchr/testify/mock" - - rpc "github.com/ethereum/go-ethereum/rpc" - - types "github.com/ethereum/go-ethereum/core/types" -) - -// SendOnlyNode is an autogenerated mock type for the SendOnlyNode type -type SendOnlyNode struct { - mock.Mock -} - -// BatchCallContext provides a mock function with given fields: ctx, b -func (_m *SendOnlyNode) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { - ret := _m.Called(ctx, b) - - if len(ret) == 0 { - panic("no return value specified for BatchCallContext") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, []rpc.BatchElem) error); ok { - r0 = rf(ctx, b) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// ChainID provides a mock function with given fields: -func (_m *SendOnlyNode) ChainID() *big.Int { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for ChainID") - } - - var r0 *big.Int - if rf, ok := ret.Get(0).(func() *big.Int); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - return r0 -} - -// Close provides a mock function with given fields: -func (_m *SendOnlyNode) Close() error { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Close") - } - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Name provides a mock function with given fields: -func (_m *SendOnlyNode) Name() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Name") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// SendTransaction provides a mock function with given fields: ctx, tx -func (_m *SendOnlyNode) SendTransaction(ctx context.Context, tx *types.Transaction) error { - ret := _m.Called(ctx, tx) - - if len(ret) == 0 { - panic("no return value specified for SendTransaction") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) error); ok { - r0 = rf(ctx, tx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Start provides a mock function with given fields: _a0 -func (_m *SendOnlyNode) Start(_a0 context.Context) error { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for Start") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(_a0) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// State provides a mock function with given fields: -func (_m *SendOnlyNode) State() client.NodeState { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for State") - } - - var r0 client.NodeState - if rf, ok := ret.Get(0).(func() client.NodeState); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(client.NodeState) - } - - return r0 -} - -// String provides a mock function with given fields: -func (_m *SendOnlyNode) String() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for String") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// NewSendOnlyNode creates a new instance of SendOnlyNode. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewSendOnlyNode(t interface { - mock.TestingT - Cleanup(func()) -}) *SendOnlyNode { - mock := &SendOnlyNode{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/core/config/docs/chains-evm.toml b/core/config/docs/chains-evm.toml index 209929b2e8b..a222d5269d7 100644 --- a/core/config/docs/chains-evm.toml +++ b/core/config/docs/chains-evm.toml @@ -15,8 +15,6 @@ BlockBackfillDepth = 10 # Default BlockBackfillSkip = false # Default # ChainType is automatically detected from chain ID. Set this to force a certain chain type regardless of chain ID. # Available types: `arbitrum`, `celo`, `gnosis`, `kroma`, `metis`, `optimismBedrock`, `scroll`, `wemix`, `xlayer`, `zksync` -# -# `xdai` has been deprecated and will be removed in v2.13.0, use `gnosis` instead. ChainType = 'arbitrum' # Example # FinalityDepth is the number of blocks after which an ethereum transaction is considered "final". Note that the default is automatically set based on chain ID, so it should not be necessary to change this under normal operation. # BlocksConsideredFinal determines how deeply we look back to ensure that transactions are confirmed onto the longest chain @@ -437,7 +435,7 @@ Order = 100 # Default # GasLimit controls the gas limit for transmit transactions from ocr2automation job. GasLimit = 5400000 # Default -[EVM.ChainWriter] +[EVM.Workflow] # FromAddress is Address of the transmitter key to use for workflow writes. FromAddress = '0x2a3e23c6f242F5345320814aC8a1b4E58707D292' # Example # ForwarderAddress is the keystone forwarder contract address on chain. diff --git a/core/config/docs/docs.go b/core/config/docs/docs.go index df082465036..5a17ab0090a 100644 --- a/core/config/docs/docs.go +++ b/core/config/docs/docs.go @@ -8,7 +8,7 @@ import ( "go.uber.org/multierr" - "github.com/smartcontractkit/chainlink/v2/core/utils" + "github.com/smartcontractkit/chainlink-common/pkg/config" ) const ( @@ -220,7 +220,7 @@ func (k keyval) String() string { } func parseTOMLDocs(s string) (items []fmt.Stringer, err error) { - defer func() { _, err = utils.MultiErrorList(err) }() + defer func() { _, err = config.MultiErrorList(err) }() globalTable := table{name: "Global"} currentTable := &globalTable items = append(items, currentTable) diff --git a/core/config/docs/docs_test.go b/core/config/docs/docs_test.go index fd59edbab6a..2e2451ab57a 100644 --- a/core/config/docs/docs_test.go +++ b/core/config/docs/docs_test.go @@ -81,10 +81,10 @@ func TestDoc(t *testing.T) { docDefaults.FlagsContractAddress = nil docDefaults.LinkContractAddress = nil docDefaults.OperatorFactoryAddress = nil - require.Empty(t, docDefaults.ChainWriter.FromAddress) - require.Empty(t, docDefaults.ChainWriter.ForwarderAddress) - docDefaults.ChainWriter.FromAddress = nil - docDefaults.ChainWriter.ForwarderAddress = nil + require.Empty(t, docDefaults.Workflow.FromAddress) + require.Empty(t, docDefaults.Workflow.ForwarderAddress) + docDefaults.Workflow.FromAddress = nil + docDefaults.Workflow.ForwarderAddress = nil docDefaults.NodePool.Errors = evmcfg.ClientErrors{} // Transactions.AutoPurge configs are only set if the feature is enabled diff --git a/core/gethwrappers/generated/automation_registry_wrapper_2_3/automation_registry_wrapper_2_3.go b/core/gethwrappers/generated/automation_registry_wrapper_2_3/automation_registry_wrapper_2_3.go index 793ea4f0184..1d460036fa0 100644 --- a/core/gethwrappers/generated/automation_registry_wrapper_2_3/automation_registry_wrapper_2_3.go +++ b/core/gethwrappers/generated/automation_registry_wrapper_2_3/automation_registry_wrapper_2_3.go @@ -76,7 +76,7 @@ type AutomationRegistryBase23PaymentReceipt struct { var AutomationRegistryMetaData = &bind.MetaData{ ABI: "[{\"inputs\":[{\"internalType\":\"contractAutomationRegistryLogicA2_3\",\"name\":\"logicA\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"ArrayHasNoEntries\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"CannotCancel\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"CheckDataExceedsLimit\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ConfigDigestMismatch\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DuplicateEntry\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DuplicateSigners\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"GasLimitCanOnlyIncrease\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"GasLimitOutsideRange\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"IncorrectNumberOfFaultyOracles\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"IncorrectNumberOfSignatures\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"IncorrectNumberOfSigners\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"IndexOutOfRange\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"available\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"requested\",\"type\":\"uint256\"}],\"name\":\"InsufficientBalance\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InsufficientLinkLiquidity\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidDataLength\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidFeed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidPayee\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidRecipient\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidReport\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidSigner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidToken\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidTransmitter\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidTrigger\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidTriggerType\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MigrationNotPermitted\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MustSettleOffchain\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MustSettleOnchain\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NotAContract\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyActiveSigners\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyActiveTransmitters\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyCallableByAdmin\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyCallableByLINKToken\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyCallableByOwnerOrAdmin\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyCallableByOwnerOrRegistrar\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyCallableByPayee\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyCallableByProposedAdmin\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyCallableByProposedPayee\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyCallableByUpkeepPrivilegeManager\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyFinanceAdmin\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyPausedUpkeep\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlySimulatedBackend\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyUnpausedUpkeep\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ParameterLengthError\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ReentrantCall\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RegistryPaused\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RepeatedSigner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RepeatedTransmitter\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"reason\",\"type\":\"bytes\"}],\"name\":\"TargetCheckReverted\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TooManyOracles\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TranscoderNotSet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TransferFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UpkeepAlreadyExists\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UpkeepCancelled\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UpkeepNotCanceled\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UpkeepNotNeeded\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ValueNotChanged\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ZeroAddressNotAllowed\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"admin\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"privilegeConfig\",\"type\":\"bytes\"}],\"name\":\"AdminPrivilegeConfigSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"gasFeePPB\",\"type\":\"uint32\"},{\"internalType\":\"uint24\",\"name\":\"flatFeeMilliCents\",\"type\":\"uint24\"}],\"indexed\":false,\"internalType\":\"structAutomationRegistryBase2_3.BillingOverrides\",\"name\":\"overrides\",\"type\":\"tuple\"}],\"name\":\"BillingConfigOverridden\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"}],\"name\":\"BillingConfigOverrideRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contractIERC20Metadata\",\"name\":\"token\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"gasFeePPB\",\"type\":\"uint32\"},{\"internalType\":\"uint24\",\"name\":\"flatFeeMilliCents\",\"type\":\"uint24\"},{\"internalType\":\"contractAggregatorV3Interface\",\"name\":\"priceFeed\",\"type\":\"address\"},{\"internalType\":\"uint8\",\"name\":\"decimals\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"fallbackPrice\",\"type\":\"uint256\"},{\"internalType\":\"uint96\",\"name\":\"minSpend\",\"type\":\"uint96\"}],\"indexed\":false,\"internalType\":\"structAutomationRegistryBase2_3.BillingConfig\",\"name\":\"config\",\"type\":\"tuple\"}],\"name\":\"BillingConfigSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"trigger\",\"type\":\"bytes\"}],\"name\":\"CancelledUpkeepReport\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newModule\",\"type\":\"address\"}],\"name\":\"ChainSpecificModuleUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"previousConfigBlockNumber\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"configDigest\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"configCount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"address[]\",\"name\":\"signers\",\"type\":\"address[]\"},{\"indexed\":false,\"internalType\":\"address[]\",\"name\":\"transmitters\",\"type\":\"address[]\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"onchainConfig\",\"type\":\"bytes\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"offchainConfigVersion\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"offchainConfig\",\"type\":\"bytes\"}],\"name\":\"ConfigSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"dedupKey\",\"type\":\"bytes32\"}],\"name\":\"DedupKeyAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"assetAddress\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"FeesWithdrawn\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"amount\",\"type\":\"uint96\"}],\"name\":\"FundsAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"FundsWithdrawn\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"trigger\",\"type\":\"bytes\"}],\"name\":\"InsufficientFundsUpkeepReport\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address[]\",\"name\":\"payees\",\"type\":\"address[]\"},{\"indexed\":false,\"internalType\":\"uint256[]\",\"name\":\"payments\",\"type\":\"uint256[]\"}],\"name\":\"NOPsSettledOffchain\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"Paused\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address[]\",\"name\":\"transmitters\",\"type\":\"address[]\"},{\"indexed\":false,\"internalType\":\"address[]\",\"name\":\"payees\",\"type\":\"address[]\"}],\"name\":\"PayeesUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transmitter\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"PayeeshipTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transmitter\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"PayeeshipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"transmitter\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"payee\",\"type\":\"address\"}],\"name\":\"PaymentWithdrawn\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"trigger\",\"type\":\"bytes\"}],\"name\":\"ReorgedUpkeepReport\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"trigger\",\"type\":\"bytes\"}],\"name\":\"StaleUpkeepReport\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"configDigest\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"epoch\",\"type\":\"uint32\"}],\"name\":\"Transmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"Unpaused\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"UpkeepAdminTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"UpkeepAdminTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"uint64\",\"name\":\"atBlockHeight\",\"type\":\"uint64\"}],\"name\":\"UpkeepCanceled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint96\",\"name\":\"gasChargeInBillingToken\",\"type\":\"uint96\"},{\"internalType\":\"uint96\",\"name\":\"premiumInBillingToken\",\"type\":\"uint96\"},{\"internalType\":\"uint96\",\"name\":\"gasReimbursementInJuels\",\"type\":\"uint96\"},{\"internalType\":\"uint96\",\"name\":\"premiumInJuels\",\"type\":\"uint96\"},{\"internalType\":\"contractIERC20Metadata\",\"name\":\"billingToken\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"linkUSD\",\"type\":\"uint96\"},{\"internalType\":\"uint96\",\"name\":\"nativeUSD\",\"type\":\"uint96\"},{\"internalType\":\"uint96\",\"name\":\"billingUSD\",\"type\":\"uint96\"}],\"indexed\":false,\"internalType\":\"structAutomationRegistryBase2_3.PaymentReceipt\",\"name\":\"receipt\",\"type\":\"tuple\"}],\"name\":\"UpkeepCharged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"newCheckData\",\"type\":\"bytes\"}],\"name\":\"UpkeepCheckDataSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"gasLimit\",\"type\":\"uint96\"}],\"name\":\"UpkeepGasLimitSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"remainingBalance\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"}],\"name\":\"UpkeepMigrated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"offchainConfig\",\"type\":\"bytes\"}],\"name\":\"UpkeepOffchainConfigSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"}],\"name\":\"UpkeepPaused\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"totalPayment\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"gasUsed\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"gasOverhead\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"trigger\",\"type\":\"bytes\"}],\"name\":\"UpkeepPerformed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"privilegeConfig\",\"type\":\"bytes\"}],\"name\":\"UpkeepPrivilegeConfigSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"startingBalance\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"importedFrom\",\"type\":\"address\"}],\"name\":\"UpkeepReceived\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"performGas\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"admin\",\"type\":\"address\"}],\"name\":\"UpkeepRegistered\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"triggerConfig\",\"type\":\"bytes\"}],\"name\":\"UpkeepTriggerConfigSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"}],\"name\":\"UpkeepUnpaused\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[],\"name\":\"acceptOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"fallbackTo\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"latestConfigDetails\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"configCount\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"blockNumber\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"configDigest\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"latestConfigDigestAndEpoch\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"scanLogs\",\"type\":\"bool\"},{\"internalType\":\"bytes32\",\"name\":\"configDigest\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"epoch\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"signers\",\"type\":\"address[]\"},{\"internalType\":\"address[]\",\"name\":\"transmitters\",\"type\":\"address[]\"},{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"internalType\":\"bytes\",\"name\":\"onchainConfigBytes\",\"type\":\"bytes\"},{\"internalType\":\"uint64\",\"name\":\"offchainConfigVersion\",\"type\":\"uint64\"},{\"internalType\":\"bytes\",\"name\":\"offchainConfig\",\"type\":\"bytes\"}],\"name\":\"setConfig\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"signers\",\"type\":\"address[]\"},{\"internalType\":\"address[]\",\"name\":\"transmitters\",\"type\":\"address[]\"},{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"checkGasLimit\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"maxPerformGas\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"maxCheckDataSize\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"transcoder\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"reorgProtectionEnabled\",\"type\":\"bool\"},{\"internalType\":\"uint24\",\"name\":\"stalenessSeconds\",\"type\":\"uint24\"},{\"internalType\":\"uint32\",\"name\":\"maxPerformDataSize\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"maxRevertDataSize\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"upkeepPrivilegeManager\",\"type\":\"address\"},{\"internalType\":\"uint16\",\"name\":\"gasCeilingMultiplier\",\"type\":\"uint16\"},{\"internalType\":\"address\",\"name\":\"financeAdmin\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"fallbackGasPrice\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"fallbackLinkPrice\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"fallbackNativePrice\",\"type\":\"uint256\"},{\"internalType\":\"address[]\",\"name\":\"registrars\",\"type\":\"address[]\"},{\"internalType\":\"contractIChainModule\",\"name\":\"chainModule\",\"type\":\"address\"}],\"internalType\":\"structAutomationRegistryBase2_3.OnchainConfig\",\"name\":\"onchainConfig\",\"type\":\"tuple\"},{\"internalType\":\"uint64\",\"name\":\"offchainConfigVersion\",\"type\":\"uint64\"},{\"internalType\":\"bytes\",\"name\":\"offchainConfig\",\"type\":\"bytes\"},{\"internalType\":\"contractIERC20Metadata[]\",\"name\":\"billingTokens\",\"type\":\"address[]\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"gasFeePPB\",\"type\":\"uint32\"},{\"internalType\":\"uint24\",\"name\":\"flatFeeMilliCents\",\"type\":\"uint24\"},{\"internalType\":\"contractAggregatorV3Interface\",\"name\":\"priceFeed\",\"type\":\"address\"},{\"internalType\":\"uint8\",\"name\":\"decimals\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"fallbackPrice\",\"type\":\"uint256\"},{\"internalType\":\"uint96\",\"name\":\"minSpend\",\"type\":\"uint96\"}],\"internalType\":\"structAutomationRegistryBase2_3.BillingConfig[]\",\"name\":\"billingConfigs\",\"type\":\"tuple[]\"}],\"name\":\"setConfigTypeSafe\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32[3]\",\"name\":\"reportContext\",\"type\":\"bytes32[3]\"},{\"internalType\":\"bytes\",\"name\":\"rawReport\",\"type\":\"bytes\"},{\"internalType\":\"bytes32[]\",\"name\":\"rs\",\"type\":\"bytes32[]\"},{\"internalType\":\"bytes32[]\",\"name\":\"ss\",\"type\":\"bytes32[]\"},{\"internalType\":\"bytes32\",\"name\":\"rawVs\",\"type\":\"bytes32\"}],\"name\":\"transmit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"typeAndVersion\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", - Bin: "0x6101806040523480156200001257600080fd5b50604051620064c0380380620064c083398101604081905262000035916200062f565b80816001600160a01b031663ca30e6036040518163ffffffff1660e01b8152600401602060405180830381865afa15801562000075573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200009b91906200062f565b826001600160a01b031663226cf83c6040518163ffffffff1660e01b8152600401602060405180830381865afa158015620000da573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200010091906200062f565b836001600160a01b031663614486af6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156200013f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200016591906200062f565b846001600160a01b0316636709d0e56040518163ffffffff1660e01b8152600401602060405180830381865afa158015620001a4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190620001ca91906200062f565b856001600160a01b0316635425d8ac6040518163ffffffff1660e01b8152600401602060405180830381865afa15801562000209573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200022f91906200062f565b866001600160a01b031663a08714c06040518163ffffffff1660e01b8152600401602060405180830381865afa1580156200026e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200029491906200062f565b876001600160a01b031663c5b964e06040518163ffffffff1660e01b8152600401602060405180830381865afa158015620002d3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190620002f9919062000656565b886001600160a01b031663ac4dc59a6040518163ffffffff1660e01b8152600401602060405180830381865afa15801562000338573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200035e91906200062f565b3380600081620003b55760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b0384811691909117909155811615620003e857620003e8816200056b565b5050506001600160a01b0380891660805287811660a05286811660c05285811660e052848116610100528316610120526025805483919060ff19166001838181111562000439576200043962000679565b0217905550806001600160a01b0316610140816001600160a01b03168152505060c0516001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa1580156200049a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190620004c091906200068f565b60ff1660a0516001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa15801562000504573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200052a91906200068f565b60ff16146200054c576040516301f86e1760e41b815260040160405180910390fd5b5050506001600160a01b039095166101605250620006b4945050505050565b336001600160a01b03821603620005c55760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c660000000000000000006044820152606401620003ac565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b6001600160a01b03811681146200062c57600080fd5b50565b6000602082840312156200064257600080fd5b81516200064f8162000616565b9392505050565b6000602082840312156200066957600080fd5b8151600281106200064f57600080fd5b634e487b7160e01b600052602160045260246000fd5b600060208284031215620006a257600080fd5b815160ff811681146200064f57600080fd5b60805160a05160c05160e05161010051610120516101405161016051615da8620007186000396000818160b30152610186015260005050600050506000505060005050600061373b015260005050600081816113120152612d640152615da86000f3fe6080604052600436106100b15760003560e01c80638da5cb5b11610069578063b1dc65a41161004e578063b1dc65a4146102cb578063e3d0e712146102eb578063f2fde38b1461030b576100b1565b80638da5cb5b1461025a578063afcb95d714610285576100b1565b8063349e8cca1161009a578063349e8cca1461017757806379ba5097146101cb57806381ff7048146101e0576100b1565b80630870d3a1146100f8578063181f5a7714610118575b7f00000000000000000000000000000000000000000000000000000000000000003660008037600080366000845af43d6000803e8080156100f1573d6000f35b3d6000fd5b005b34801561010457600080fd5b506100f6610113366004614b1c565b61032b565b34801561012457600080fd5b506101616040518060400160405280601881526020017f4175746f6d6174696f6e526567697374727920322e332e30000000000000000081525081565b60405161016e9190614c98565b60405180910390f35b34801561018357600080fd5b507f00000000000000000000000000000000000000000000000000000000000000005b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200161016e565b3480156101d757600080fd5b506100f6610c0f565b3480156101ec57600080fd5b5061023760175460135463ffffffff74010000000000000000000000000000000000000000830481169378010000000000000000000000000000000000000000000000009093041691565b6040805163ffffffff94851681529390921660208401529082015260600161016e565b34801561026657600080fd5b5060005473ffffffffffffffffffffffffffffffffffffffff166101a6565b34801561029157600080fd5b50601354601454604080516000815260208101939093526c0100000000000000000000000090910463ffffffff169082015260600161016e565b3480156102d757600080fd5b506100f66102e6366004614cf7565b610d11565b3480156102f757600080fd5b506100f6610306366004614ddc565b610ff2565b34801561031757600080fd5b506100f6610326366004614ea9565b61102c565b610333611040565b601f8851111561036f576040517f25d0209c00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8560ff166000036103ac576040517fe77dba5600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b865188511415806103cb57506103c3866003614ef5565b60ff16885111155b15610402576040517f1d2d1c5800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805182511461043d576040517fcf54c06a00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61044782826110c3565b61045188886116fb565b604051806101200160405280601460000160009054906101000a90046bffffffffffffffffffffffff166bffffffffffffffffffffffff168152602001600063ffffffff1681526020018660a0015162ffffff16815260200186610120015161ffff1681526020018760ff168152602001601460000160169054906101000a900460ff1615158152602001601460000160179054906101000a900460ff1615158152602001866080015115158152602001866101e0015173ffffffffffffffffffffffffffffffffffffffff16815250601460008201518160000160006101000a8154816bffffffffffffffffffffffff02191690836bffffffffffffffffffffffff160217905550602082015181600001600c6101000a81548163ffffffff021916908363ffffffff16021790555060408201518160000160106101000a81548162ffffff021916908362ffffff16021790555060608201518160000160136101000a81548161ffff021916908361ffff16021790555060808201518160000160156101000a81548160ff021916908360ff16021790555060a08201518160000160166101000a81548160ff02191690831515021790555060c08201518160000160176101000a81548160ff02191690831515021790555060e08201518160000160186101000a81548160ff0219169083151502179055506101008201518160010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055509050506000601660010160189054906101000a900463ffffffff1690506000866101e0015173ffffffffffffffffffffffffffffffffffffffff166357e871e76040518163ffffffff1660e01b8152600401602060405180830381865afa158015610701573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107259190614f11565b6017549091506000906107579074010000000000000000000000000000000000000000900463ffffffff166001614f2a565b9050604051806101600160405280896060015173ffffffffffffffffffffffffffffffffffffffff168152602001896000015163ffffffff168152602001896020015163ffffffff1681526020016016600001601c9054906101000a900463ffffffff1663ffffffff16815260200189610100015173ffffffffffffffffffffffffffffffffffffffff1681526020018263ffffffff1681526020018363ffffffff168152602001896040015163ffffffff16815260200189610140015173ffffffffffffffffffffffffffffffffffffffff1681526020018960c0015163ffffffff1681526020018960e0015163ffffffff16815250601660008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060208201518160000160146101000a81548163ffffffff021916908363ffffffff16021790555060408201518160000160186101000a81548163ffffffff021916908363ffffffff160217905550606082015181600001601c6101000a81548163ffffffff021916908363ffffffff16021790555060808201518160010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060a08201518160010160146101000a81548163ffffffff021916908363ffffffff16021790555060c08201518160010160186101000a81548163ffffffff021916908363ffffffff16021790555060e082015181600101601c6101000a81548163ffffffff021916908363ffffffff1602179055506101008201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101208201518160020160146101000a81548163ffffffff021916908363ffffffff1602179055506101408201518160020160186101000a81548163ffffffff021916908363ffffffff160217905550905050876101600151601981905550876101800151601a81905550876101a00151601b81905550600088604051602001610a9a9190614f98565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0018152919052601754909150610aff904690309074010000000000000000000000000000000000000000900463ffffffff168f8f8f878f8f611dba565b6013556000610b0e6009611e64565b90505b8015610b4b57610b38610b30610b2860018461511e565b600990611e74565b600990611e87565b5080610b4381615131565b915050610b11565b5060005b896101c0015151811015610ba257610b8f8a6101c001518281518110610b7757610b77615166565b60200260200101516009611ea990919063ffffffff16565b5080610b9a81615195565b915050610b4f565b507f1591690b8638f5fb2dbec82ac741805ac5da8b45dc5263f4875b0496fdce4e0584601354601660010160149054906101000a900463ffffffff168f8f8f878f8f604051610bf9999897969594939291906151cd565b60405180910390a1505050505050505050505050565b60015473ffffffffffffffffffffffffffffffffffffffff163314610c95576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e65720000000000000000000060448201526064015b60405180910390fd5b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b60005a60408051610120810182526014546bffffffffffffffffffffffff8116825263ffffffff6c01000000000000000000000000820416602083015262ffffff7001000000000000000000000000000000008204169282019290925261ffff730100000000000000000000000000000000000000830416606082015260ff75010000000000000000000000000000000000000000008304811660808301527601000000000000000000000000000000000000000000008304811615801560a08401527701000000000000000000000000000000000000000000000084048216151560c0840152780100000000000000000000000000000000000000000000000090930416151560e082015260155473ffffffffffffffffffffffffffffffffffffffff16610100820152919250610e75576040517f24522f3400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b336000908152600b602052604090205460ff16610ebe576040517f1099ed7500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6013548a3514610efa576040517fdfdcf8e700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6080810151610f0a906001615263565b60ff1686141580610f1b5750858414155b15610f52576040517f0244f71a00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610f628a8a8a8a8a8a8a8a611ecb565b6000610f6e8a8a612134565b905060208b0135600881901c63ffffffff16610f8b8484876121ed565b836020015163ffffffff168163ffffffff161115610fe357601480547fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff166c0100000000000000000000000063ffffffff8416021790555b50505050505050505050505050565b60008060008580602001905181019061100b91906153e9565b925092509250611021898989868989888861032b565b505050505050505050565b611034611040565b61103d81612dda565b50565b60005473ffffffffffffffffffffffffffffffffffffffff1633146110c1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e6572000000000000000000006044820152606401610c8c565b565b60005b6024548110156111815760226000602483815481106110e7576110e7615166565b600091825260208083209091015473ffffffffffffffffffffffffffffffffffffffff168352820192909252604001812080547fffffffff00000000000000000000000000000000000000000000000000000000168155600181019190915560020180547fffffffffffffffffffffffffffffffffffffffff0000000000000000000000001690558061117981615195565b9150506110c6565b5061118e60246000614570565b60255460ff1660005b83518110156116f55760008482815181106111b4576111b4615166565b6020026020010151905060008483815181106111d2576111d2615166565b602002602001015190508173ffffffffffffffffffffffffffffffffffffffff1663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa158015611227573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061124b9190615594565b60ff16816060015160ff161415806112d95750806040015173ffffffffffffffffffffffffffffffffffffffff1663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa1580156112ad573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112d19190615594565b60ff16600814155b15611310576040517fc1ab6dc100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614801561137c5750600184600181111561137a5761137a6155b1565b145b156113b3576040517fc1ab6dc100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff821615806113ee5750604081015173ffffffffffffffffffffffffffffffffffffffff16155b15611425576040517f8579befe00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8281166000908152602260205260409020546701000000000000009004161561148f576040517f357d0cc400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6024805460018181019092557f7cd332d19b93bcabe3cce7ca0c18a052f57e5fd03b4758a09f30f5ddc4b22ec401805473ffffffffffffffffffffffffffffffffffffffff8086167fffffffffffffffffffffffff0000000000000000000000000000000000000000909216821790925560008181526022602090815260409182902086518154928801518489015160608a015160ff167b01000000000000000000000000000000000000000000000000000000027fffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffff9190981667010000000000000002167fffffffff000000000000000000000000000000000000000000ffffffffffffff62ffffff909216640100000000027fffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000090951663ffffffff9093169290921793909317929092169190911793909317835560808501519383019390935560a0840151600290920180546bffffffffffffffffffffffff9093167fffffffffffffffffffffffffffffffffffffffff0000000000000000000000009093169290921790915590517fca93cbe727c73163ec538f71be6c0a64877d7f1f6dd35d5ca7cbaef3a3e34ba3906116d8908490600060c08201905063ffffffff835116825262ffffff602084015116602083015273ffffffffffffffffffffffffffffffffffffffff604084015116604083015260ff6060840151166060830152608083015160808301526bffffffffffffffffffffffff60a08401511660a083015292915050565b60405180910390a2505080806116ed90615195565b915050611197565b50505050565b600e546014546bffffffffffffffffffffffff1660005b600e5481101561176e5761175b600e828154811061173257611732615166565b60009182526020909120015473ffffffffffffffffffffffffffffffffffffffff168385612ecf565b508061176681615195565b915050611712565b5060255460009060ff16815b600e548110156118df57600e818154811061179757611797615166565b6000918252602082200154600d805473ffffffffffffffffffffffffffffffffffffffff9092169550600c9291849081106117d4576117d4615166565b600091825260208083209091015473ffffffffffffffffffffffffffffffffffffffff9081168452838201949094526040928301822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00001690559286168152600b909252902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690556001826001811115611876576118766155b1565b1480156118bb575073ffffffffffffffffffffffffffffffffffffffff83166000908152600b60205260409020546201000090046bffffffffffffffffffffffff1615155b156118cd576118cb600f84611ea9565b505b806118d781615195565b91505061177a565b506118ec600d6000614570565b6118f8600e6000614570565b6040805160808101825260008082526020820181905291810182905260608101829052905b8751811015611d8857600c600089838151811061193c5761193c615166565b60209081029190910181015173ffffffffffffffffffffffffffffffffffffffff1682528101919091526040016000205460ff16156119a7576040517f77cea0fa00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168882815181106119d1576119d1615166565b602002602001015173ffffffffffffffffffffffffffffffffffffffff1603611a26576040517f815e1d6400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60405180604001604052806001151581526020018260ff16815250600c60008a8481518110611a5757611a57615166565b60209081029190910181015173ffffffffffffffffffffffffffffffffffffffff1682528181019290925260400160002082518154939092015160ff16610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff921515929092167fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000909316929092171790558651879082908110611aff57611aff615166565b60200260200101519350600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1603611b6f576040517f58a70a0a00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff84166000908152600b60209081526040918290208251608081018452905460ff80821615801584526101008304909116938301939093526bffffffffffffffffffffffff6201000082048116948301949094526e01000000000000000000000000000090049092166060830152909250611c2a576040517f6a7281ad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600180835260ff80831660208086019182526014546bffffffffffffffffffffffff9081166060880190815273ffffffffffffffffffffffffffffffffffffffff8a166000908152600b909352604092839020885181549551948a0151925184166e010000000000000000000000000000027fffffffffffff000000000000000000000000ffffffffffffffffffffffffffff939094166201000002929092167fffffffffffff000000000000000000000000000000000000000000000000ffff94909616610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff921515929092167fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000090951694909417179190911692909217919091179055836001811115611d6457611d646155b1565b03611d7657611d74600f85611e87565b505b80611d8081615195565b91505061191d565b508651611d9c90600d9060208a019061458e565b508551611db090600e90602089019061458e565b5050505050505050565b6000808a8a8a8a8a8a8a8a8a604051602001611dde999897969594939291906155e0565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001815291905280516020909101207dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff167e01000000000000000000000000000000000000000000000000000000000000179b9a5050505050505050505050565b6000611e6e825490565b92915050565b6000611e8083836130d7565b9392505050565b6000611e808373ffffffffffffffffffffffffffffffffffffffff8416613101565b6000611e808373ffffffffffffffffffffffffffffffffffffffff84166131fb565b60008787604051611edd929190615675565b604051908190038120611ef4918b90602001615685565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081840301815282825280516020918201208383019092526000808452908301819052909250906000805b888110156120cb57600185878360208110611f6057611f60615166565b611f6d91901a601b615263565b8c8c85818110611f7f57611f7f615166565b905060200201358b8b86818110611f9857611f98615166565b9050602002013560405160008152602001604052604051611fd5949392919093845260ff9290921660208401526040830152606082015260800190565b6020604051602081039080840390855afa158015611ff7573d6000803e3d6000fd5b5050604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081015173ffffffffffffffffffffffffffffffffffffffff81166000908152600c602090815290849020838501909452925460ff80821615158085526101009092041693830193909352909550935090506120a5576040517f0f4c073700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b826020015160080260ff166001901b8401935080806120c390615195565b915050611f43565b50827e01010101010101010101010101010101010101010101010101010101010101841614612126576040517fc103be2e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b505050505050505050505050565b61216d6040518060c001604052806000815260200160008152602001606081526020016060815260200160608152602001606081525090565b600061217b83850185615776565b604081015151606082015151919250908114158061219e57508082608001515114155b806121ae5750808260a001515114155b156121e5576040517fb55ac75400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b509392505050565b600082604001515167ffffffffffffffff81111561220d5761220d614625565b6040519080825280602002602001820160405280156122d957816020015b6040805161020081018252600060e08201818152610100830182905261012083018290526101408301829052610160830182905261018083018290526101a083018290526101c083018290526101e0830182905282526020808301829052928201819052606082018190526080820181905260a0820181905260c082015282527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff90920191018161222b5790505b50905060006040518060800160405280600061ffff16815260200160006bffffffffffffffffffffffff16815260200160006bffffffffffffffffffffffff16815260200160008152509050600085610100015173ffffffffffffffffffffffffffffffffffffffff166357e871e76040518163ffffffff1660e01b8152600401602060405180830381865afa158015612377573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061239b9190614f11565b9050600086610100015173ffffffffffffffffffffffffffffffffffffffff166318b8f6136040518163ffffffff1660e01b8152600401602060405180830381865afa1580156123ef573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906124139190614f11565b905060005b8660400151518110156128a357600460008860400151838151811061243f5761243f615166565b6020908102919091018101518252818101929092526040908101600020815161012081018352815460ff8082161515835261010080830490911615159583019590955263ffffffff620100008204811694830194909452660100000000000081048416606083015273ffffffffffffffffffffffffffffffffffffffff6a01000000000000000000009091048116608083015260018301546fffffffffffffffffffffffffffffffff811660a08401526bffffffffffffffffffffffff70010000000000000000000000000000000082041660c08401527c0100000000000000000000000000000000000000000000000000000000900490931660e082015260029091015490911691810191909152855186908390811061256257612562615166565b6020026020010151600001819052506125978760400151828151811061258a5761258a615166565b602002602001015161324a565b8582815181106125a9576125a9615166565b60200260200101516060019060018111156125c6576125c66155b1565b908160018111156125d9576125d96155b1565b8152505061263d876040015182815181106125f6576125f6615166565b6020026020010151848960800151848151811061261557612615615166565b602002602001015188858151811061262f5761262f615166565b60200260200101518c6132f5565b86838151811061264f5761264f615166565b602002602001015160200187848151811061266c5761266c615166565b602002602001015160c001828152508215151515815250505084818151811061269757612697615166565b602002602001015160200151156126c7576001846000018181516126bb9190615863565b61ffff169052506126cc565b612891565b6127328582815181106126e1576126e1615166565b602002602001015160000151608001518860600151838151811061270757612707615166565b60200260200101518960a00151848151811061272557612725615166565b6020026020010151613414565b86838151811061274457612744615166565b602002602001015160400187848151811061276157612761615166565b602002602001015160800182815250821515151581525050508760800151600161278b9190615263565b6127999060ff16604061587e565b6103a48860a0015183815181106127b2576127b2615166565b6020026020010151516127c59190615895565b6127cf9190615895565b8582815181106127e1576127e1615166565b602002602001015160a001818152505084818151811061280357612803615166565b602002602001015160a00151846060018181516128209190615895565b905250845185908290811061283757612837615166565b6020026020010151608001518661284e919061511e565b95506128918760400151828151811061286957612869615166565b60200260200101518487848151811061288457612884615166565b602002602001015161362f565b8061289b81615195565b915050612418565b50825161ffff166000036128ba5750505050505050565b61c8006128c836601061587e565b5a6128d3908861511e565b6128dd9190615895565b6128e79190615895565b8351909550613778906128fe9061ffff16876158d7565b6129089190615895565b604080516080810182526000808252602082018190529181018290526060810182905291965061293789613734565b905060005b886040015151811015612c735786818151811061295b5761295b615166565b60200260200101516020015115612c61578015806129f357508661298060018361511e565b8151811061299057612990615166565b602002602001015160000151610100015173ffffffffffffffffffffffffffffffffffffffff168782815181106129c9576129c9615166565b602002602001015160000151610100015173ffffffffffffffffffffffffffffffffffffffff1614155b15612a2757612a248a888381518110612a0e57612a0e615166565b602002602001015160000151610100015161381e565b92505b6000612b458b6040518061012001604052808b8681518110612a4b57612a4b615166565b60200260200101516080015181526020018c81526020018a606001518c8781518110612a7957612a79615166565b602002602001015160a001518a612a90919061587e565b612a9a91906158d7565b81526020018d6000015181526020018d6020015181526020018681526020018b8681518110612acb57612acb615166565b602002602001015160000151610100015173ffffffffffffffffffffffffffffffffffffffff168152602001878152602001600115158152508c604001518581518110612b1a57612b1a615166565b60200260200101518b8681518110612b3457612b34615166565b60200260200101516000015161399a565b9050806060015187604001818151612b5d91906158eb565b6bffffffffffffffffffffffff169052506040810151602088018051612b849083906158eb565b6bffffffffffffffffffffffff169052508751889083908110612ba957612ba9615166565b60200260200101516040015115158a604001518381518110612bcd57612bcd615166565b60200260200101517fad8cc9579b21dfe2c2f6ea35ba15b656e46b4f5b0cb424f52739b8ce5cac9c5b83602001518460000151612c0a91906158eb565b8b8681518110612c1c57612c1c615166565b6020026020010151608001518d8f608001518881518110612c3f57612c3f615166565b6020026020010151604051612c579493929190615910565b60405180910390a3505b80612c6b81615195565b91505061293c565b505050602083810151336000908152600b90925260409091208054600290612cb09084906201000090046bffffffffffffffffffffffff166158eb565b92506101000a8154816bffffffffffffffffffffffff02191690836bffffffffffffffffffffffff1602179055508260400151601460000160008282829054906101000a90046bffffffffffffffffffffffff16612d0e91906158eb565b92506101000a8154816bffffffffffffffffffffffff02191690836bffffffffffffffffffffffff16021790555082604001518360200151612d5091906158eb565b6bffffffffffffffffffffffff16602160007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254612dcc9190615895565b909155505050505050505050565b3373ffffffffffffffffffffffffffffffffffffffff821603612e59576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c660000000000000000006044820152606401610c8c565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b73ffffffffffffffffffffffffffffffffffffffff83166000908152600b602090815260408083208151608081018352905460ff80821615801584526101008304909116948301949094526bffffffffffffffffffffffff6201000082048116938301939093526e01000000000000000000000000000090049091166060820152906130cb576000816060015185612f67919061594d565b90506000612f758583615972565b90508083604001818151612f8991906158eb565b6bffffffffffffffffffffffff16905250612fa4858261599d565b83606001818151612fb591906158eb565b6bffffffffffffffffffffffff90811690915273ffffffffffffffffffffffffffffffffffffffff89166000908152600b602090815260409182902087518154928901519389015160608a015186166e010000000000000000000000000000027fffffffffffff000000000000000000000000ffffffffffffffffffffffffffff919096166201000002167fffffffffffff000000000000000000000000000000000000000000000000ffff60ff95909516610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff921515929092167fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000909416939093171792909216179190911790555050505b60400151949350505050565b60008260000182815481106130ee576130ee615166565b9060005260206000200154905092915050565b600081815260018301602052604081205480156131ea57600061312560018361511e565b85549091506000906131399060019061511e565b905081811461319e57600086600001828154811061315957613159615166565b906000526020600020015490508087600001848154811061317c5761317c615166565b6000918252602080832090910192909255918252600188019052604090208390555b85548690806131af576131af6159cd565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050611e6e565b6000915050611e6e565b5092915050565b600081815260018301602052604081205461324257508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155611e6e565b506000611e6e565b6000818160045b600f8110156132d7577fff00000000000000000000000000000000000000000000000000000000000000821683826020811061328f5761328f615166565b1a60f81b7effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916146132c557506000949350505050565b806132cf81615195565b915050613251565b5081600f1a60018111156132ed576132ed6155b1565b949350505050565b600080808085606001516001811115613310576133106155b1565b03613336576133228888888888613e09565b6133315760009250905061340a565b6133ae565b60018560600151600181111561334e5761334e6155b1565b0361337c57600061336189898988613f93565b9250905080613376575060009250905061340a565b506133ae565b6040517ff2b2d41200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b84516060015163ffffffff16871061340357877fc3237c8807c467c1b39b8d0395eff077313e691bf0a7388106792564ebfd5636876040516133f09190614c98565b60405180910390a260009250905061340a565b6001925090505b9550959350505050565b601454600090819077010000000000000000000000000000000000000000000000900460ff1615613471576040517f37ed32e800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b601480547fffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffff16770100000000000000000000000000000000000000000000001790556040517f4585e33b00000000000000000000000000000000000000000000000000000000906134e6908590602401614c98565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009094169390931790925290517f79188d1600000000000000000000000000000000000000000000000000000000815290935073ffffffffffffffffffffffffffffffffffffffff8616906379188d16906135b990879087906004016159fc565b60408051808303816000875af11580156135d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906135fb9190615a15565b601480547fffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffff16905590969095509350505050565b600081606001516001811115613647576136476155b1565b036136ab57600083815260046020526040902060010180547bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167c010000000000000000000000000000000000000000000000000000000063ffffffff851602179055505050565b6001816060015160018111156136c3576136c36155b1565b0361372f5760c08101805160009081526008602052604080822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055915191517fa4a4e334c0e330143f9437484fe516c13bc560b86b5b0daf58e7084aaac228f29190a25b505050565b60008060007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff1663feaf968c6040518163ffffffff1660e01b815260040160a060405180830381865afa1580156137a4573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906137c89190615a5d565b509350509250506000821315806137de57508042105b8061380e57506000846040015162ffffff1611801561380e5750613802814261511e565b846040015162ffffff16105b156131f4575050601b5492915050565b60408051608081018252600080825260208083018281528385018381526060850184905273ffffffffffffffffffffffffffffffffffffffff878116855260229093528584208054640100000000810462ffffff1690925263ffffffff82169092527b01000000000000000000000000000000000000000000000000000000810460ff16855285517ffeaf968c00000000000000000000000000000000000000000000000000000000815295519495919484936701000000000000009092049091169163feaf968c9160048083019260a09291908290030181865afa15801561390b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061392f9190615a5d565b5093505092505060008213158061394557508042105b8061397557506000866040015162ffffff161180156139755750613969814261511e565b866040015162ffffff16105b156139895760018301546060850152613991565b606084018290525b50505092915050565b604080516101008101825260008082526020808301829052928201819052606082018190526080820181905260a0820181905260c0820181905260e08201529082015115613a305760008381526023602090815260409182902082518084018452905463ffffffff811680835262ffffff640100000000909204821692840192835260e089018051909401529051915191169101525b6000613a3c86866141a0565b60c0840151602082015182519293509091600091613a59916158eb565b60e08801515190915060ff16600060128210613a76576001613a8c565b613a8182601261511e565b613a8c90600a615bcd565b9050600060128311613a9f576001613ab5565b613aaa60128461511e565b613ab590600a615bcd565b905085600001516bffffffffffffffffffffffff16856bffffffffffffffffffffffff161015613b5d57849350613b31818b60800151613af5919061587e565b838c60e0015160600151886bffffffffffffffffffffffff16613b18919061587e565b613b22919061587e565b613b2c91906158d7565b6144ce565b6bffffffffffffffffffffffff9081166040880152600060608801819052602088015285168652613c83565b836bffffffffffffffffffffffff16856bffffffffffffffffffffffff161015613c8357849350613beb86604001516bffffffffffffffffffffffff16828c60800151613baa919061587e565b848d60e0015160600151896bffffffffffffffffffffffff16613bcd919061587e565b613bd7919061587e565b613be191906158d7565b613b2c919061511e565b6bffffffffffffffffffffffff1660608088019190915260e08b01510151613c6f90613c1890849061587e565b6001848d60e0015160600151613c2e919061587e565b613c38919061511e565b838d608001518a606001516bffffffffffffffffffffffff16613c5b919061587e565b613c65919061587e565b613b229190615895565b6bffffffffffffffffffffffff1660208701525b60008981526004602052604090206001018054859190601090613cc990849070010000000000000000000000000000000090046bffffffffffffffffffffffff1661594d565b82546101009290920a6bffffffffffffffffffffffff81810219909316918316021790915560008b81526004602052604081206001018054928816935091613d249084906fffffffffffffffffffffffffffffffff16615bd9565b92506101000a8154816fffffffffffffffffffffffffffffffff02191690836fffffffffffffffffffffffffffffffff160217905550836bffffffffffffffffffffffff16602160008c60c0015173ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254613dbb919061511e565b92505081905550887f801ba6ed51146ffe3e99d1dbd9dd0f4de6292e78a9a34c39c0183de17b3f40fc87604051613df29190615c02565b60405180910390a250939998505050505050505050565b60008084806020019051810190613e209190615cc2565b845160e00151815191925063ffffffff90811691161015613e7d57867f405288ea7be309e16cfdf481367f90a413e1d4634fcdaf8966546db9b93012e886604051613e6b9190614c98565b60405180910390a26000915050613f8a565b8260e001518015613f3d5750602081015115801590613f3d5750602081015161010084015182516040517f85df51fd00000000000000000000000000000000000000000000000000000000815263ffffffff909116600482015273ffffffffffffffffffffffffffffffffffffffff909116906385df51fd90602401602060405180830381865afa158015613f16573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613f3a9190614f11565b14155b80613f4f5750805163ffffffff168611155b15613f8457867f6aa7f60c176da7af894b384daea2249497448137f5943c1237ada8bc92bdc30186604051613e6b9190614c98565b60019150505b95945050505050565b600080600084806020019051810190613fac9190615d1a565b905060008782600001518360200151846040015160405160200161400e94939291909384526020840192909252604083015260e01b7fffffffff0000000000000000000000000000000000000000000000000000000016606082015260640190565b6040516020818303038152906040528051906020012090508460e0015180156140e957506080820151158015906140e95750608082015161010086015160608401516040517f85df51fd00000000000000000000000000000000000000000000000000000000815263ffffffff909116600482015273ffffffffffffffffffffffffffffffffffffffff909116906385df51fd90602401602060405180830381865afa1580156140c2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906140e69190614f11565b14155b806140fe575086826060015163ffffffff1610155b1561414857877f6aa7f60c176da7af894b384daea2249497448137f5943c1237ada8bc92bdc301876040516141339190614c98565b60405180910390a26000935091506141979050565b60008181526008602052604090205460ff161561418f57877f405288ea7be309e16cfdf481367f90a413e1d4634fcdaf8966546db9b93012e8876040516141339190614c98565b600193509150505b94509492505050565b6040805161010081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c0810182905260e081019190915260008260e001516000015160ff1690506000846060015161ffff16846060015161420b919061587e565b9050836101000151801561421e5750803a105b1561422657503a5b60006012831161423757600161424d565b61424260128461511e565b61424d90600a615bcd565b9050600060128410614260576001614276565b61426b84601261511e565b61427690600a615bcd565b905060008660a001518760400151886020015189600001516142989190615895565b6142a2908761587e565b6142ac9190615895565b6142b6919061587e565b90506142f9828860e00151606001516142cf919061587e565b6001848a60e00151606001516142e5919061587e565b6142ef919061511e565b613c65868561587e565b6bffffffffffffffffffffffff168652608087015161431c90613b2c90836158d7565b6bffffffffffffffffffffffff1660408088019190915260e088015101516000906143559062ffffff16683635c9adc5dea0000061587e565b9050600081633b9aca008a60a001518b60e001516020015163ffffffff168c604001518d600001518b614388919061587e565b6143929190615895565b61439c919061587e565b6143a6919061587e565b6143b091906158d7565b6143ba9190615895565b90506143fd848a60e00151606001516143d3919061587e565b6001868c60e00151606001516143e9919061587e565b6143f3919061511e565b613c65888561587e565b6bffffffffffffffffffffffff166020890152608089015161442390613b2c90836158d7565b6bffffffffffffffffffffffff16606089015260c089015173ffffffffffffffffffffffffffffffffffffffff166080808a0191909152890151614466906144ce565b6bffffffffffffffffffffffff1660a0808a0191909152890151614489906144ce565b6bffffffffffffffffffffffff1660c089015260e0890151606001516144ae906144ce565b6bffffffffffffffffffffffff1660e08901525050505050505092915050565b60006bffffffffffffffffffffffff82111561456c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f53616665436173743a2076616c756520646f65736e27742066697420696e203960448201527f36206269747300000000000000000000000000000000000000000000000000006064820152608401610c8c565b5090565b508054600082559060005260206000209081019061103d9190614610565b828054828255906000526020600020908101928215614608579160200282015b8281111561460857825182547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9091161782556020909201916001909101906145ae565b5061456c9291505b5b8082111561456c5760008155600101614611565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b604051610200810167ffffffffffffffff8111828210171561467857614678614625565b60405290565b60405160c0810167ffffffffffffffff8111828210171561467857614678614625565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff811182821017156146e8576146e8614625565b604052919050565b600067ffffffffffffffff82111561470a5761470a614625565b5060051b60200190565b73ffffffffffffffffffffffffffffffffffffffff8116811461103d57600080fd5b803561474181614714565b919050565b600082601f83011261475757600080fd5b8135602061476c614767836146f0565b6146a1565b82815260059290921b8401810191818101908684111561478b57600080fd5b8286015b848110156147af5780356147a281614714565b835291830191830161478f565b509695505050505050565b60ff8116811461103d57600080fd5b8035614741816147ba565b63ffffffff8116811461103d57600080fd5b8035614741816147d4565b801515811461103d57600080fd5b8035614741816147f1565b62ffffff8116811461103d57600080fd5b80356147418161480a565b61ffff8116811461103d57600080fd5b803561474181614826565b6000610200828403121561485457600080fd5b61485c614654565b9050614867826147e6565b8152614875602083016147e6565b6020820152614886604083016147e6565b604082015261489760608301614736565b60608201526148a8608083016147ff565b60808201526148b960a0830161481b565b60a08201526148ca60c083016147e6565b60c08201526148db60e083016147e6565b60e08201526101006148ee818401614736565b90820152610120614900838201614836565b90820152610140614912838201614736565b90820152610160828101359082015261018080830135908201526101a080830135908201526101c08083013567ffffffffffffffff81111561495357600080fd5b61495f85828601614746565b8284015250506101e0614973818401614736565b9082015292915050565b803567ffffffffffffffff8116811461474157600080fd5b600082601f8301126149a657600080fd5b813567ffffffffffffffff8111156149c0576149c0614625565b6149f160207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116016146a1565b818152846020838601011115614a0657600080fd5b816020850160208301376000918101602001919091529392505050565b6bffffffffffffffffffffffff8116811461103d57600080fd5b600082601f830112614a4e57600080fd5b81356020614a5e614767836146f0565b82815260c09283028501820192828201919087851115614a7d57600080fd5b8387015b85811015614b0f5781818a031215614a995760008081fd5b614aa161467e565b8135614aac816147d4565b815281860135614abb8161480a565b81870152604082810135614ace81614714565b90820152606082810135614ae1816147ba565b908201526080828101359082015260a080830135614afe81614a23565b908201528452928401928101614a81565b5090979650505050505050565b600080600080600080600080610100898b031215614b3957600080fd5b883567ffffffffffffffff80821115614b5157600080fd5b614b5d8c838d01614746565b995060208b0135915080821115614b7357600080fd5b614b7f8c838d01614746565b9850614b8d60408c016147c9565b975060608b0135915080821115614ba357600080fd5b614baf8c838d01614841565b9650614bbd60808c0161497d565b955060a08b0135915080821115614bd357600080fd5b614bdf8c838d01614995565b945060c08b0135915080821115614bf557600080fd5b614c018c838d01614746565b935060e08b0135915080821115614c1757600080fd5b50614c248b828c01614a3d565b9150509295985092959890939650565b6000815180845260005b81811015614c5a57602081850181015186830182015201614c3e565b5060006020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b602081526000611e806020830184614c34565b60008083601f840112614cbd57600080fd5b50813567ffffffffffffffff811115614cd557600080fd5b6020830191508360208260051b8501011115614cf057600080fd5b9250929050565b60008060008060008060008060e0898b031215614d1357600080fd5b606089018a811115614d2457600080fd5b8998503567ffffffffffffffff80821115614d3e57600080fd5b818b0191508b601f830112614d5257600080fd5b813581811115614d6157600080fd5b8c6020828501011115614d7357600080fd5b6020830199508098505060808b0135915080821115614d9157600080fd5b614d9d8c838d01614cab565b909750955060a08b0135915080821115614db657600080fd5b50614dc38b828c01614cab565b999c989b50969995989497949560c00135949350505050565b60008060008060008060c08789031215614df557600080fd5b863567ffffffffffffffff80821115614e0d57600080fd5b614e198a838b01614746565b97506020890135915080821115614e2f57600080fd5b614e3b8a838b01614746565b9650614e4960408a016147c9565b95506060890135915080821115614e5f57600080fd5b614e6b8a838b01614995565b9450614e7960808a0161497d565b935060a0890135915080821115614e8f57600080fd5b50614e9c89828a01614995565b9150509295509295509295565b600060208284031215614ebb57600080fd5b8135611e8081614714565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60ff81811683821602908116908181146131f4576131f4614ec6565b600060208284031215614f2357600080fd5b5051919050565b63ffffffff8181168382160190808211156131f4576131f4614ec6565b600081518084526020808501945080840160005b83811015614f8d57815173ffffffffffffffffffffffffffffffffffffffff1687529582019590820190600101614f5b565b509495945050505050565b60208152614faf60208201835163ffffffff169052565b60006020830151614fc8604084018263ffffffff169052565b50604083015163ffffffff8116606084015250606083015173ffffffffffffffffffffffffffffffffffffffff8116608084015250608083015180151560a08401525060a083015162ffffff811660c08401525060c083015163ffffffff811660e08401525060e08301516101006150478185018363ffffffff169052565b84015190506101206150708482018373ffffffffffffffffffffffffffffffffffffffff169052565b84015190506101406150878482018361ffff169052565b84015190506101606150b08482018373ffffffffffffffffffffffffffffffffffffffff169052565b840151610180848101919091528401516101a0808501919091528401516101c0808501919091528401516102006101e0808601829052919250906150f8610220860184614f47565b95015173ffffffffffffffffffffffffffffffffffffffff169301929092525090919050565b81810381811115611e6e57611e6e614ec6565b60008161514057615140614ec6565b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036151c6576151c6614ec6565b5060010190565b600061012063ffffffff808d1684528b6020850152808b166040850152508060608401526151fd8184018a614f47565b905082810360808401526152118189614f47565b905060ff871660a084015282810360c084015261522e8187614c34565b905067ffffffffffffffff851660e08401528281036101008401526152538185614c34565b9c9b505050505050505050505050565b60ff8181168382160190811115611e6e57611e6e614ec6565b8051614741816147d4565b805161474181614714565b8051614741816147f1565b80516147418161480a565b805161474181614826565b600082601f8301126152c457600080fd5b815160206152d4614767836146f0565b82815260059290921b840181019181810190868411156152f357600080fd5b8286015b848110156147af57805161530a81614714565b83529183019183016152f7565b600082601f83011261532857600080fd5b81516020615338614767836146f0565b82815260c0928302850182019282820191908785111561535757600080fd5b8387015b85811015614b0f5781818a0312156153735760008081fd5b61537b61467e565b8151615386816147d4565b8152818601516153958161480a565b818701526040828101516153a881614714565b908201526060828101516153bb816147ba565b908201526080828101519082015260a0808301516153d881614a23565b90820152845292840192810161535b565b6000806000606084860312156153fe57600080fd5b835167ffffffffffffffff8082111561541657600080fd5b90850190610200828803121561542b57600080fd5b615433614654565b61543c8361527c565b815261544a6020840161527c565b602082015261545b6040840161527c565b604082015261546c60608401615287565b606082015261547d60808401615292565b608082015261548e60a0840161529d565b60a082015261549f60c0840161527c565b60c08201526154b060e0840161527c565b60e08201526101006154c3818501615287565b908201526101206154d58482016152a8565b908201526101406154e7848201615287565b90820152610160838101519082015261018080840151908201526101a080840151908201526101c0808401518381111561552057600080fd5b61552c8a8287016152b3565b8284015250506101e0615540818501615287565b90820152602087015190955091508082111561555b57600080fd5b615567878388016152b3565b9350604086015191508082111561557d57600080fd5b5061558a86828701615317565b9150509250925092565b6000602082840312156155a657600080fd5b8151611e80816147ba565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b60006101208b835273ffffffffffffffffffffffffffffffffffffffff8b16602084015267ffffffffffffffff808b1660408501528160608501526156278285018b614f47565b9150838203608085015261563b828a614f47565b915060ff881660a085015283820360c08501526156588288614c34565b90861660e085015283810361010085015290506152538185614c34565b8183823760009101908152919050565b8281526080810160608360208401379392505050565b600082601f8301126156ac57600080fd5b813560206156bc614767836146f0565b82815260059290921b840181019181810190868411156156db57600080fd5b8286015b848110156147af57803583529183019183016156df565b600082601f83011261570757600080fd5b81356020615717614767836146f0565b82815260059290921b8401810191818101908684111561573657600080fd5b8286015b848110156147af57803567ffffffffffffffff81111561575a5760008081fd5b6157688986838b0101614995565b84525091830191830161573a565b60006020828403121561578857600080fd5b813567ffffffffffffffff808211156157a057600080fd5b9083019060c082860312156157b457600080fd5b6157bc61467e565b82358152602083013560208201526040830135828111156157dc57600080fd5b6157e88782860161569b565b60408301525060608301358281111561580057600080fd5b61580c8782860161569b565b60608301525060808301358281111561582457600080fd5b615830878286016156f6565b60808301525060a08301358281111561584857600080fd5b615854878286016156f6565b60a08301525095945050505050565b61ffff8181168382160190808211156131f4576131f4614ec6565b8082028115828204841417611e6e57611e6e614ec6565b80820180821115611e6e57611e6e614ec6565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b6000826158e6576158e66158a8565b500490565b6bffffffffffffffffffffffff8181168382160190808211156131f4576131f4614ec6565b6bffffffffffffffffffffffff851681528360208201528260408201526080606082015260006159436080830184614c34565b9695505050505050565b6bffffffffffffffffffffffff8281168282160390808211156131f4576131f4614ec6565b60006bffffffffffffffffffffffff80841680615991576159916158a8565b92169190910492915050565b6bffffffffffffffffffffffff8181168382160280821691908281146159c5576159c5614ec6565b505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b8281526040602082015260006132ed6040830184614c34565b60008060408385031215615a2857600080fd5b8251615a33816147f1565b6020939093015192949293505050565b805169ffffffffffffffffffff8116811461474157600080fd5b600080600080600060a08688031215615a7557600080fd5b615a7e86615a43565b9450602086015193506040860151925060608601519150615aa160808701615a43565b90509295509295909350565b600181815b80851115615b0657817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115615aec57615aec614ec6565b80851615615af957918102915b93841c9390800290615ab2565b509250929050565b600082615b1d57506001611e6e565b81615b2a57506000611e6e565b8160018114615b405760028114615b4a57615b66565b6001915050611e6e565b60ff841115615b5b57615b5b614ec6565b50506001821b611e6e565b5060208310610133831016604e8410600b8410161715615b89575081810a611e6e565b615b938383615aad565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115615bc557615bc5614ec6565b029392505050565b6000611e808383615b0e565b6fffffffffffffffffffffffffffffffff8181168382160190808211156131f4576131f4614ec6565b6000610100820190506bffffffffffffffffffffffff8084511683528060208501511660208401528060408501511660408401528060608501511660608401525073ffffffffffffffffffffffffffffffffffffffff608084015116608083015260a0830151615c8260a08401826bffffffffffffffffffffffff169052565b5060c0830151615ca260c08401826bffffffffffffffffffffffff169052565b5060e08301516131f460e08401826bffffffffffffffffffffffff169052565b600060408284031215615cd457600080fd5b6040516040810181811067ffffffffffffffff82111715615cf757615cf7614625565b6040528251615d05816147d4565b81526020928301519281019290925250919050565b600060a08284031215615d2c57600080fd5b60405160a0810181811067ffffffffffffffff82111715615d4f57615d4f614625565b806040525082518152602083015160208201526040830151615d70816147d4565b60408201526060830151615d83816147d4565b6060820152608092830151928101929092525091905056fea164736f6c6343000813000a", + Bin: "0x6101806040523480156200001257600080fd5b506040516200651f3803806200651f83398101604081905262000035916200062f565b80816001600160a01b031663ca30e6036040518163ffffffff1660e01b8152600401602060405180830381865afa15801562000075573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200009b91906200062f565b826001600160a01b031663226cf83c6040518163ffffffff1660e01b8152600401602060405180830381865afa158015620000da573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200010091906200062f565b836001600160a01b031663614486af6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156200013f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200016591906200062f565b846001600160a01b0316636709d0e56040518163ffffffff1660e01b8152600401602060405180830381865afa158015620001a4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190620001ca91906200062f565b856001600160a01b0316635425d8ac6040518163ffffffff1660e01b8152600401602060405180830381865afa15801562000209573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200022f91906200062f565b866001600160a01b031663a08714c06040518163ffffffff1660e01b8152600401602060405180830381865afa1580156200026e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200029491906200062f565b876001600160a01b031663c5b964e06040518163ffffffff1660e01b8152600401602060405180830381865afa158015620002d3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190620002f9919062000656565b886001600160a01b031663ac4dc59a6040518163ffffffff1660e01b8152600401602060405180830381865afa15801562000338573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200035e91906200062f565b3380600081620003b55760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b0384811691909117909155811615620003e857620003e8816200056b565b5050506001600160a01b0380891660805287811660a05286811660c05285811660e052848116610100528316610120526025805483919060ff19166001838181111562000439576200043962000679565b0217905550806001600160a01b0316610140816001600160a01b03168152505060c0516001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa1580156200049a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190620004c091906200068f565b60ff1660a0516001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa15801562000504573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906200052a91906200068f565b60ff16146200054c576040516301f86e1760e41b815260040160405180910390fd5b5050506001600160a01b039095166101605250620006b4945050505050565b336001600160a01b03821603620005c55760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c660000000000000000006044820152606401620003ac565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b6001600160a01b03811681146200062c57600080fd5b50565b6000602082840312156200064257600080fd5b81516200064f8162000616565b9392505050565b6000602082840312156200066957600080fd5b8151600281106200064f57600080fd5b634e487b7160e01b600052602160045260246000fd5b600060208284031215620006a257600080fd5b815160ff811681146200064f57600080fd5b60805160a05160c05160e05161010051610120516101405161016051615e07620007186000396000818160b30152610186015260005050600050506000505060005050600061379a015260005050600081816113710152612dc30152615e076000f3fe6080604052600436106100b15760003560e01c80638da5cb5b11610069578063b1dc65a41161004e578063b1dc65a4146102cb578063e3d0e712146102eb578063f2fde38b1461030b576100b1565b80638da5cb5b1461025a578063afcb95d714610285576100b1565b8063349e8cca1161009a578063349e8cca1461017757806379ba5097146101cb57806381ff7048146101e0576100b1565b80630870d3a1146100f8578063181f5a7714610118575b7f00000000000000000000000000000000000000000000000000000000000000003660008037600080366000845af43d6000803e8080156100f1573d6000f35b3d6000fd5b005b34801561010457600080fd5b506100f6610113366004614b7b565b61032b565b34801561012457600080fd5b506101616040518060400160405280601881526020017f4175746f6d6174696f6e526567697374727920322e332e30000000000000000081525081565b60405161016e9190614cf7565b60405180910390f35b34801561018357600080fd5b507f00000000000000000000000000000000000000000000000000000000000000005b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200161016e565b3480156101d757600080fd5b506100f6610c0f565b3480156101ec57600080fd5b5061023760175460135463ffffffff74010000000000000000000000000000000000000000830481169378010000000000000000000000000000000000000000000000009093041691565b6040805163ffffffff94851681529390921660208401529082015260600161016e565b34801561026657600080fd5b5060005473ffffffffffffffffffffffffffffffffffffffff166101a6565b34801561029157600080fd5b50601354601454604080516000815260208101939093526c0100000000000000000000000090910463ffffffff169082015260600161016e565b3480156102d757600080fd5b506100f66102e6366004614d56565b610d11565b3480156102f757600080fd5b506100f6610306366004614e3b565b611051565b34801561031757600080fd5b506100f6610326366004614f08565b61108b565b61033361109f565b601f8851111561036f576040517f25d0209c00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8560ff166000036103ac576040517fe77dba5600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b865188511415806103cb57506103c3866003614f54565b60ff16885111155b15610402576040517f1d2d1c5800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805182511461043d576040517fcf54c06a00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6104478282611122565b610451888861175a565b604051806101200160405280601460000160009054906101000a90046bffffffffffffffffffffffff166bffffffffffffffffffffffff168152602001600063ffffffff1681526020018660a0015162ffffff16815260200186610120015161ffff1681526020018760ff168152602001601460000160169054906101000a900460ff1615158152602001601460000160179054906101000a900460ff1615158152602001866080015115158152602001866101e0015173ffffffffffffffffffffffffffffffffffffffff16815250601460008201518160000160006101000a8154816bffffffffffffffffffffffff02191690836bffffffffffffffffffffffff160217905550602082015181600001600c6101000a81548163ffffffff021916908363ffffffff16021790555060408201518160000160106101000a81548162ffffff021916908362ffffff16021790555060608201518160000160136101000a81548161ffff021916908361ffff16021790555060808201518160000160156101000a81548160ff021916908360ff16021790555060a08201518160000160166101000a81548160ff02191690831515021790555060c08201518160000160176101000a81548160ff02191690831515021790555060e08201518160000160186101000a81548160ff0219169083151502179055506101008201518160010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055509050506000601660010160189054906101000a900463ffffffff1690506000866101e0015173ffffffffffffffffffffffffffffffffffffffff166357e871e76040518163ffffffff1660e01b8152600401602060405180830381865afa158015610701573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107259190614f70565b6017549091506000906107579074010000000000000000000000000000000000000000900463ffffffff166001614f89565b9050604051806101600160405280896060015173ffffffffffffffffffffffffffffffffffffffff168152602001896000015163ffffffff168152602001896020015163ffffffff1681526020016016600001601c9054906101000a900463ffffffff1663ffffffff16815260200189610100015173ffffffffffffffffffffffffffffffffffffffff1681526020018263ffffffff1681526020018363ffffffff168152602001896040015163ffffffff16815260200189610140015173ffffffffffffffffffffffffffffffffffffffff1681526020018960c0015163ffffffff1681526020018960e0015163ffffffff16815250601660008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060208201518160000160146101000a81548163ffffffff021916908363ffffffff16021790555060408201518160000160186101000a81548163ffffffff021916908363ffffffff160217905550606082015181600001601c6101000a81548163ffffffff021916908363ffffffff16021790555060808201518160010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060a08201518160010160146101000a81548163ffffffff021916908363ffffffff16021790555060c08201518160010160186101000a81548163ffffffff021916908363ffffffff16021790555060e082015181600101601c6101000a81548163ffffffff021916908363ffffffff1602179055506101008201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101208201518160020160146101000a81548163ffffffff021916908363ffffffff1602179055506101408201518160020160186101000a81548163ffffffff021916908363ffffffff160217905550905050876101600151601981905550876101800151601a81905550876101a00151601b81905550600088604051602001610a9a9190614ff7565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0018152919052601754909150610aff904690309074010000000000000000000000000000000000000000900463ffffffff168f8f8f878f8f611e19565b6013556000610b0e6009611ec3565b90505b8015610b4b57610b38610b30610b2860018461517d565b600990611ed3565b600990611ee6565b5080610b4381615190565b915050610b11565b5060005b896101c0015151811015610ba257610b8f8a6101c001518281518110610b7757610b776151c5565b60200260200101516009611f0890919063ffffffff16565b5080610b9a816151f4565b915050610b4f565b507f1591690b8638f5fb2dbec82ac741805ac5da8b45dc5263f4875b0496fdce4e0584601354601660010160149054906101000a900463ffffffff168f8f8f878f8f604051610bf99998979695949392919061522c565b60405180910390a1505050505050505050505050565b60015473ffffffffffffffffffffffffffffffffffffffff163314610c95576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e65720000000000000000000060448201526064015b60405180910390fd5b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b60005a90506000610d238660406152c2565b610d2f896101446152d9565b610d3991906152d9565b9050368114610d74576040517fdfe9309000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60408051610120810182526014546bffffffffffffffffffffffff8116825263ffffffff6c01000000000000000000000000820416602083015262ffffff7001000000000000000000000000000000008204169282019290925261ffff730100000000000000000000000000000000000000830416606082015260ff75010000000000000000000000000000000000000000008304811660808301527601000000000000000000000000000000000000000000008304811615801560a08401527701000000000000000000000000000000000000000000000084048216151560c0840152780100000000000000000000000000000000000000000000000090930416151560e082015260155473ffffffffffffffffffffffffffffffffffffffff1661010082015290610ed3576040517f24522f3400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b336000908152600b602052604090205460ff16610f1c576040517f1099ed7500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6013548b3514610f58576040517fdfdcf8e700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6080810151610f689060016152ec565b60ff1687141580610f795750868514155b15610fb0576040517f0244f71a00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610fc08b8b8b8b8b8b8b8b611f2a565b6000610fcc8b8b612193565b905060208c0135600881901c63ffffffff16610fe984848861224c565b836020015163ffffffff168163ffffffff16111561104157601480547fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff166c0100000000000000000000000063ffffffff8416021790555b5050505050505050505050505050565b60008060008580602001905181019061106a9190615472565b925092509250611080898989868989888861032b565b505050505050505050565b61109361109f565b61109c81612e39565b50565b60005473ffffffffffffffffffffffffffffffffffffffff163314611120576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e6572000000000000000000006044820152606401610c8c565b565b60005b6024548110156111e0576022600060248381548110611146576111466151c5565b600091825260208083209091015473ffffffffffffffffffffffffffffffffffffffff168352820192909252604001812080547fffffffff00000000000000000000000000000000000000000000000000000000168155600181019190915560020180547fffffffffffffffffffffffffffffffffffffffff000000000000000000000000169055806111d8816151f4565b915050611125565b506111ed602460006145cf565b60255460ff1660005b8351811015611754576000848281518110611213576112136151c5565b602002602001015190506000848381518110611231576112316151c5565b602002602001015190508173ffffffffffffffffffffffffffffffffffffffff1663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa158015611286573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112aa919061561d565b60ff16816060015160ff161415806113385750806040015173ffffffffffffffffffffffffffffffffffffffff1663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa15801561130c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611330919061561d565b60ff16600814155b1561136f576040517fc1ab6dc100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161480156113db575060018460018111156113d9576113d961563a565b145b15611412576040517fc1ab6dc100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216158061144d5750604081015173ffffffffffffffffffffffffffffffffffffffff16155b15611484576040517f8579befe00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff828116600090815260226020526040902054670100000000000000900416156114ee576040517f357d0cc400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6024805460018181019092557f7cd332d19b93bcabe3cce7ca0c18a052f57e5fd03b4758a09f30f5ddc4b22ec401805473ffffffffffffffffffffffffffffffffffffffff8086167fffffffffffffffffffffffff0000000000000000000000000000000000000000909216821790925560008181526022602090815260409182902086518154928801518489015160608a015160ff167b01000000000000000000000000000000000000000000000000000000027fffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffff9190981667010000000000000002167fffffffff000000000000000000000000000000000000000000ffffffffffffff62ffffff909216640100000000027fffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000090951663ffffffff9093169290921793909317929092169190911793909317835560808501519383019390935560a0840151600290920180546bffffffffffffffffffffffff9093167fffffffffffffffffffffffffffffffffffffffff0000000000000000000000009093169290921790915590517fca93cbe727c73163ec538f71be6c0a64877d7f1f6dd35d5ca7cbaef3a3e34ba390611737908490600060c08201905063ffffffff835116825262ffffff602084015116602083015273ffffffffffffffffffffffffffffffffffffffff604084015116604083015260ff6060840151166060830152608083015160808301526bffffffffffffffffffffffff60a08401511660a083015292915050565b60405180910390a25050808061174c906151f4565b9150506111f6565b50505050565b600e546014546bffffffffffffffffffffffff1660005b600e548110156117cd576117ba600e8281548110611791576117916151c5565b60009182526020909120015473ffffffffffffffffffffffffffffffffffffffff168385612f2e565b50806117c5816151f4565b915050611771565b5060255460009060ff16815b600e5481101561193e57600e81815481106117f6576117f66151c5565b6000918252602082200154600d805473ffffffffffffffffffffffffffffffffffffffff9092169550600c929184908110611833576118336151c5565b600091825260208083209091015473ffffffffffffffffffffffffffffffffffffffff9081168452838201949094526040928301822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00001690559286168152600b909252902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016905560018260018111156118d5576118d561563a565b14801561191a575073ffffffffffffffffffffffffffffffffffffffff83166000908152600b60205260409020546201000090046bffffffffffffffffffffffff1615155b1561192c5761192a600f84611f08565b505b80611936816151f4565b9150506117d9565b5061194b600d60006145cf565b611957600e60006145cf565b6040805160808101825260008082526020820181905291810182905260608101829052905b8751811015611de757600c600089838151811061199b5761199b6151c5565b60209081029190910181015173ffffffffffffffffffffffffffffffffffffffff1682528101919091526040016000205460ff1615611a06576040517f77cea0fa00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff16888281518110611a3057611a306151c5565b602002602001015173ffffffffffffffffffffffffffffffffffffffff1603611a85576040517f815e1d6400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60405180604001604052806001151581526020018260ff16815250600c60008a8481518110611ab657611ab66151c5565b60209081029190910181015173ffffffffffffffffffffffffffffffffffffffff1682528181019290925260400160002082518154939092015160ff16610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff921515929092167fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000909316929092171790558651879082908110611b5e57611b5e6151c5565b60200260200101519350600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1603611bce576040517f58a70a0a00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff84166000908152600b60209081526040918290208251608081018452905460ff80821615801584526101008304909116938301939093526bffffffffffffffffffffffff6201000082048116948301949094526e01000000000000000000000000000090049092166060830152909250611c89576040517f6a7281ad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600180835260ff80831660208086019182526014546bffffffffffffffffffffffff9081166060880190815273ffffffffffffffffffffffffffffffffffffffff8a166000908152600b909352604092839020885181549551948a0151925184166e010000000000000000000000000000027fffffffffffff000000000000000000000000ffffffffffffffffffffffffffff939094166201000002929092167fffffffffffff000000000000000000000000000000000000000000000000ffff94909616610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff921515929092167fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000090951694909417179190911692909217919091179055836001811115611dc357611dc361563a565b03611dd557611dd3600f85611ee6565b505b80611ddf816151f4565b91505061197c565b508651611dfb90600d9060208a01906145ed565b508551611e0f90600e9060208901906145ed565b5050505050505050565b6000808a8a8a8a8a8a8a8a8a604051602001611e3d99989796959493929190615669565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001815291905280516020909101207dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff167e01000000000000000000000000000000000000000000000000000000000000179b9a5050505050505050505050565b6000611ecd825490565b92915050565b6000611edf8383613136565b9392505050565b6000611edf8373ffffffffffffffffffffffffffffffffffffffff8416613160565b6000611edf8373ffffffffffffffffffffffffffffffffffffffff841661325a565b60008787604051611f3c9291906156fe565b604051908190038120611f53918b9060200161570e565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081840301815282825280516020918201208383019092526000808452908301819052909250906000805b8881101561212a57600185878360208110611fbf57611fbf6151c5565b611fcc91901a601b6152ec565b8c8c85818110611fde57611fde6151c5565b905060200201358b8b86818110611ff757611ff76151c5565b9050602002013560405160008152602001604052604051612034949392919093845260ff9290921660208401526040830152606082015260800190565b6020604051602081039080840390855afa158015612056573d6000803e3d6000fd5b5050604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081015173ffffffffffffffffffffffffffffffffffffffff81166000908152600c602090815290849020838501909452925460ff8082161515808552610100909204169383019390935290955093509050612104576040517f0f4c073700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b826020015160080260ff166001901b840193508080612122906151f4565b915050611fa2565b50827e01010101010101010101010101010101010101010101010101010101010101841614612185576040517fc103be2e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b505050505050505050505050565b6121cc6040518060c001604052806000815260200160008152602001606081526020016060815260200160608152602001606081525090565b60006121da838501856157ff565b60408101515160608201515191925090811415806121fd57508082608001515114155b8061220d5750808260a001515114155b15612244576040517fb55ac75400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b509392505050565b600082604001515167ffffffffffffffff81111561226c5761226c614684565b60405190808252806020026020018201604052801561233857816020015b6040805161020081018252600060e08201818152610100830182905261012083018290526101408301829052610160830182905261018083018290526101a083018290526101c083018290526101e0830182905282526020808301829052928201819052606082018190526080820181905260a0820181905260c082015282527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff90920191018161228a5790505b50905060006040518060800160405280600061ffff16815260200160006bffffffffffffffffffffffff16815260200160006bffffffffffffffffffffffff16815260200160008152509050600085610100015173ffffffffffffffffffffffffffffffffffffffff166357e871e76040518163ffffffff1660e01b8152600401602060405180830381865afa1580156123d6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123fa9190614f70565b9050600086610100015173ffffffffffffffffffffffffffffffffffffffff166318b8f6136040518163ffffffff1660e01b8152600401602060405180830381865afa15801561244e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906124729190614f70565b905060005b86604001515181101561290257600460008860400151838151811061249e5761249e6151c5565b6020908102919091018101518252818101929092526040908101600020815161012081018352815460ff8082161515835261010080830490911615159583019590955263ffffffff620100008204811694830194909452660100000000000081048416606083015273ffffffffffffffffffffffffffffffffffffffff6a01000000000000000000009091048116608083015260018301546fffffffffffffffffffffffffffffffff811660a08401526bffffffffffffffffffffffff70010000000000000000000000000000000082041660c08401527c0100000000000000000000000000000000000000000000000000000000900490931660e08201526002909101549091169181019190915285518690839081106125c1576125c16151c5565b6020026020010151600001819052506125f6876040015182815181106125e9576125e96151c5565b60200260200101516132a9565b858281518110612608576126086151c5565b60200260200101516060019060018111156126255761262561563a565b908160018111156126385761263861563a565b8152505061269c87604001518281518110612655576126556151c5565b60200260200101518489608001518481518110612674576126746151c5565b602002602001015188858151811061268e5761268e6151c5565b60200260200101518c613354565b8683815181106126ae576126ae6151c5565b60200260200101516020018784815181106126cb576126cb6151c5565b602002602001015160c00182815250821515151581525050508481815181106126f6576126f66151c5565b602002602001015160200151156127265760018460000181815161271a91906158ec565b61ffff1690525061272b565b6128f0565b612791858281518110612740576127406151c5565b6020026020010151600001516080015188606001518381518110612766576127666151c5565b60200260200101518960a001518481518110612784576127846151c5565b6020026020010151613473565b8683815181106127a3576127a36151c5565b60200260200101516040018784815181106127c0576127c06151c5565b60200260200101516080018281525082151515158152505050876080015160016127ea91906152ec565b6127f89060ff1660406152c2565b6103a48860a001518381518110612811576128116151c5565b60200260200101515161282491906152d9565b61282e91906152d9565b858281518110612840576128406151c5565b602002602001015160a0018181525050848181518110612862576128626151c5565b602002602001015160a001518460600181815161287f91906152d9565b9052508451859082908110612896576128966151c5565b602002602001015160800151866128ad919061517d565b95506128f0876040015182815181106128c8576128c86151c5565b6020026020010151848784815181106128e3576128e36151c5565b602002602001015161368e565b806128fa816151f4565b915050612477565b50825161ffff166000036129195750505050505050565b61c8006129273660106152c2565b5a612932908861517d565b61293c91906152d9565b61294691906152d9565b83519095506137789061295d9061ffff1687615936565b61296791906152d9565b604080516080810182526000808252602082018190529181018290526060810182905291965061299689613793565b905060005b886040015151811015612cd2578681815181106129ba576129ba6151c5565b60200260200101516020015115612cc057801580612a525750866129df60018361517d565b815181106129ef576129ef6151c5565b602002602001015160000151610100015173ffffffffffffffffffffffffffffffffffffffff16878281518110612a2857612a286151c5565b602002602001015160000151610100015173ffffffffffffffffffffffffffffffffffffffff1614155b15612a8657612a838a888381518110612a6d57612a6d6151c5565b602002602001015160000151610100015161387d565b92505b6000612ba48b6040518061012001604052808b8681518110612aaa57612aaa6151c5565b60200260200101516080015181526020018c81526020018a606001518c8781518110612ad857612ad86151c5565b602002602001015160a001518a612aef91906152c2565b612af99190615936565b81526020018d6000015181526020018d6020015181526020018681526020018b8681518110612b2a57612b2a6151c5565b602002602001015160000151610100015173ffffffffffffffffffffffffffffffffffffffff168152602001878152602001600115158152508c604001518581518110612b7957612b796151c5565b60200260200101518b8681518110612b9357612b936151c5565b6020026020010151600001516139f9565b9050806060015187604001818151612bbc919061594a565b6bffffffffffffffffffffffff169052506040810151602088018051612be390839061594a565b6bffffffffffffffffffffffff169052508751889083908110612c0857612c086151c5565b60200260200101516040015115158a604001518381518110612c2c57612c2c6151c5565b60200260200101517fad8cc9579b21dfe2c2f6ea35ba15b656e46b4f5b0cb424f52739b8ce5cac9c5b83602001518460000151612c69919061594a565b8b8681518110612c7b57612c7b6151c5565b6020026020010151608001518d8f608001518881518110612c9e57612c9e6151c5565b6020026020010151604051612cb6949392919061596f565b60405180910390a3505b80612cca816151f4565b91505061299b565b505050602083810151336000908152600b90925260409091208054600290612d0f9084906201000090046bffffffffffffffffffffffff1661594a565b92506101000a8154816bffffffffffffffffffffffff02191690836bffffffffffffffffffffffff1602179055508260400151601460000160008282829054906101000a90046bffffffffffffffffffffffff16612d6d919061594a565b92506101000a8154816bffffffffffffffffffffffff02191690836bffffffffffffffffffffffff16021790555082604001518360200151612daf919061594a565b6bffffffffffffffffffffffff16602160007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254612e2b91906152d9565b909155505050505050505050565b3373ffffffffffffffffffffffffffffffffffffffff821603612eb8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c660000000000000000006044820152606401610c8c565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b73ffffffffffffffffffffffffffffffffffffffff83166000908152600b602090815260408083208151608081018352905460ff80821615801584526101008304909116948301949094526bffffffffffffffffffffffff6201000082048116938301939093526e010000000000000000000000000000900490911660608201529061312a576000816060015185612fc691906159ac565b90506000612fd485836159d1565b90508083604001818151612fe8919061594a565b6bffffffffffffffffffffffff1690525061300385826159fc565b83606001818151613014919061594a565b6bffffffffffffffffffffffff90811690915273ffffffffffffffffffffffffffffffffffffffff89166000908152600b602090815260409182902087518154928901519389015160608a015186166e010000000000000000000000000000027fffffffffffff000000000000000000000000ffffffffffffffffffffffffffff919096166201000002167fffffffffffff000000000000000000000000000000000000000000000000ffff60ff95909516610100027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff921515929092167fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000909416939093171792909216179190911790555050505b60400151949350505050565b600082600001828154811061314d5761314d6151c5565b9060005260206000200154905092915050565b6000818152600183016020526040812054801561324957600061318460018361517d565b85549091506000906131989060019061517d565b90508181146131fd5760008660000182815481106131b8576131b86151c5565b90600052602060002001549050808760000184815481106131db576131db6151c5565b6000918252602080832090910192909255918252600188019052604090208390555b855486908061320e5761320e615a2c565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050611ecd565b6000915050611ecd565b5092915050565b60008181526001830160205260408120546132a157508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155611ecd565b506000611ecd565b6000818160045b600f811015613336577fff0000000000000000000000000000000000000000000000000000000000000082168382602081106132ee576132ee6151c5565b1a60f81b7effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff19161461332457506000949350505050565b8061332e816151f4565b9150506132b0565b5081600f1a600181111561334c5761334c61563a565b949350505050565b60008080808560600151600181111561336f5761336f61563a565b03613395576133818888888888613e68565b61339057600092509050613469565b61340d565b6001856060015160018111156133ad576133ad61563a565b036133db5760006133c089898988613ff2565b92509050806133d55750600092509050613469565b5061340d565b6040517ff2b2d41200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b84516060015163ffffffff16871061346257877fc3237c8807c467c1b39b8d0395eff077313e691bf0a7388106792564ebfd56368760405161344f9190614cf7565b60405180910390a2600092509050613469565b6001925090505b9550959350505050565b601454600090819077010000000000000000000000000000000000000000000000900460ff16156134d0576040517f37ed32e800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b601480547fffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffff16770100000000000000000000000000000000000000000000001790556040517f4585e33b0000000000000000000000000000000000000000000000000000000090613545908590602401614cf7565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009094169390931790925290517f79188d1600000000000000000000000000000000000000000000000000000000815290935073ffffffffffffffffffffffffffffffffffffffff8616906379188d16906136189087908790600401615a5b565b60408051808303816000875af1158015613636573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061365a9190615a74565b601480547fffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffff16905590969095509350505050565b6000816060015160018111156136a6576136a661563a565b0361370a57600083815260046020526040902060010180547bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167c010000000000000000000000000000000000000000000000000000000063ffffffff851602179055505050565b6001816060015160018111156137225761372261563a565b0361378e5760c08101805160009081526008602052604080822080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055915191517fa4a4e334c0e330143f9437484fe516c13bc560b86b5b0daf58e7084aaac228f29190a25b505050565b60008060007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff1663feaf968c6040518163ffffffff1660e01b815260040160a060405180830381865afa158015613803573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906138279190615abc565b5093505092505060008213158061383d57508042105b8061386d57506000846040015162ffffff1611801561386d5750613861814261517d565b846040015162ffffff16105b15613253575050601b5492915050565b60408051608081018252600080825260208083018281528385018381526060850184905273ffffffffffffffffffffffffffffffffffffffff878116855260229093528584208054640100000000810462ffffff1690925263ffffffff82169092527b01000000000000000000000000000000000000000000000000000000810460ff16855285517ffeaf968c00000000000000000000000000000000000000000000000000000000815295519495919484936701000000000000009092049091169163feaf968c9160048083019260a09291908290030181865afa15801561396a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061398e9190615abc565b509350509250506000821315806139a457508042105b806139d457506000866040015162ffffff161180156139d457506139c8814261517d565b866040015162ffffff16105b156139e857600183015460608501526139f0565b606084018290525b50505092915050565b604080516101008101825260008082526020808301829052928201819052606082018190526080820181905260a0820181905260c0820181905260e08201529082015115613a8f5760008381526023602090815260409182902082518084018452905463ffffffff811680835262ffffff640100000000909204821692840192835260e089018051909401529051915191169101525b6000613a9b86866141ff565b60c0840151602082015182519293509091600091613ab89161594a565b60e08801515190915060ff16600060128210613ad5576001613aeb565b613ae082601261517d565b613aeb90600a615c2c565b9050600060128311613afe576001613b14565b613b0960128461517d565b613b1490600a615c2c565b905085600001516bffffffffffffffffffffffff16856bffffffffffffffffffffffff161015613bbc57849350613b90818b60800151613b5491906152c2565b838c60e0015160600151886bffffffffffffffffffffffff16613b7791906152c2565b613b8191906152c2565b613b8b9190615936565b61452d565b6bffffffffffffffffffffffff9081166040880152600060608801819052602088015285168652613ce2565b836bffffffffffffffffffffffff16856bffffffffffffffffffffffff161015613ce257849350613c4a86604001516bffffffffffffffffffffffff16828c60800151613c0991906152c2565b848d60e0015160600151896bffffffffffffffffffffffff16613c2c91906152c2565b613c3691906152c2565b613c409190615936565b613b8b919061517d565b6bffffffffffffffffffffffff1660608088019190915260e08b01510151613cce90613c779084906152c2565b6001848d60e0015160600151613c8d91906152c2565b613c97919061517d565b838d608001518a606001516bffffffffffffffffffffffff16613cba91906152c2565b613cc491906152c2565b613b8191906152d9565b6bffffffffffffffffffffffff1660208701525b60008981526004602052604090206001018054859190601090613d2890849070010000000000000000000000000000000090046bffffffffffffffffffffffff166159ac565b82546101009290920a6bffffffffffffffffffffffff81810219909316918316021790915560008b81526004602052604081206001018054928816935091613d839084906fffffffffffffffffffffffffffffffff16615c38565b92506101000a8154816fffffffffffffffffffffffffffffffff02191690836fffffffffffffffffffffffffffffffff160217905550836bffffffffffffffffffffffff16602160008c60c0015173ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254613e1a919061517d565b92505081905550887f801ba6ed51146ffe3e99d1dbd9dd0f4de6292e78a9a34c39c0183de17b3f40fc87604051613e519190615c61565b60405180910390a250939998505050505050505050565b60008084806020019051810190613e7f9190615d21565b845160e00151815191925063ffffffff90811691161015613edc57867f405288ea7be309e16cfdf481367f90a413e1d4634fcdaf8966546db9b93012e886604051613eca9190614cf7565b60405180910390a26000915050613fe9565b8260e001518015613f9c5750602081015115801590613f9c5750602081015161010084015182516040517f85df51fd00000000000000000000000000000000000000000000000000000000815263ffffffff909116600482015273ffffffffffffffffffffffffffffffffffffffff909116906385df51fd90602401602060405180830381865afa158015613f75573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613f999190614f70565b14155b80613fae5750805163ffffffff168611155b15613fe357867f6aa7f60c176da7af894b384daea2249497448137f5943c1237ada8bc92bdc30186604051613eca9190614cf7565b60019150505b95945050505050565b60008060008480602001905181019061400b9190615d79565b905060008782600001518360200151846040015160405160200161406d94939291909384526020840192909252604083015260e01b7fffffffff0000000000000000000000000000000000000000000000000000000016606082015260640190565b6040516020818303038152906040528051906020012090508460e00151801561414857506080820151158015906141485750608082015161010086015160608401516040517f85df51fd00000000000000000000000000000000000000000000000000000000815263ffffffff909116600482015273ffffffffffffffffffffffffffffffffffffffff909116906385df51fd90602401602060405180830381865afa158015614121573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906141459190614f70565b14155b8061415d575086826060015163ffffffff1610155b156141a757877f6aa7f60c176da7af894b384daea2249497448137f5943c1237ada8bc92bdc301876040516141929190614cf7565b60405180910390a26000935091506141f69050565b60008181526008602052604090205460ff16156141ee57877f405288ea7be309e16cfdf481367f90a413e1d4634fcdaf8966546db9b93012e8876040516141929190614cf7565b600193509150505b94509492505050565b6040805161010081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c0810182905260e081019190915260008260e001516000015160ff1690506000846060015161ffff16846060015161426a91906152c2565b9050836101000151801561427d5750803a105b1561428557503a5b6000601283116142965760016142ac565b6142a160128461517d565b6142ac90600a615c2c565b90506000601284106142bf5760016142d5565b6142ca84601261517d565b6142d590600a615c2c565b905060008660a001518760400151886020015189600001516142f791906152d9565b61430190876152c2565b61430b91906152d9565b61431591906152c2565b9050614358828860e001516060015161432e91906152c2565b6001848a60e001516060015161434491906152c2565b61434e919061517d565b613cc486856152c2565b6bffffffffffffffffffffffff168652608087015161437b90613b8b9083615936565b6bffffffffffffffffffffffff1660408088019190915260e088015101516000906143b49062ffffff16683635c9adc5dea000006152c2565b9050600081633b9aca008a60a001518b60e001516020015163ffffffff168c604001518d600001518b6143e791906152c2565b6143f191906152d9565b6143fb91906152c2565b61440591906152c2565b61440f9190615936565b61441991906152d9565b905061445c848a60e001516060015161443291906152c2565b6001868c60e001516060015161444891906152c2565b614452919061517d565b613cc488856152c2565b6bffffffffffffffffffffffff166020890152608089015161448290613b8b9083615936565b6bffffffffffffffffffffffff16606089015260c089015173ffffffffffffffffffffffffffffffffffffffff166080808a01919091528901516144c59061452d565b6bffffffffffffffffffffffff1660a0808a01919091528901516144e89061452d565b6bffffffffffffffffffffffff1660c089015260e08901516060015161450d9061452d565b6bffffffffffffffffffffffff1660e08901525050505050505092915050565b60006bffffffffffffffffffffffff8211156145cb576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f53616665436173743a2076616c756520646f65736e27742066697420696e203960448201527f36206269747300000000000000000000000000000000000000000000000000006064820152608401610c8c565b5090565b508054600082559060005260206000209081019061109c919061466f565b828054828255906000526020600020908101928215614667579160200282015b8281111561466757825182547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff90911617825560209092019160019091019061460d565b506145cb9291505b5b808211156145cb5760008155600101614670565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b604051610200810167ffffffffffffffff811182821017156146d7576146d7614684565b60405290565b60405160c0810167ffffffffffffffff811182821017156146d7576146d7614684565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff8111828210171561474757614747614684565b604052919050565b600067ffffffffffffffff82111561476957614769614684565b5060051b60200190565b73ffffffffffffffffffffffffffffffffffffffff8116811461109c57600080fd5b80356147a081614773565b919050565b600082601f8301126147b657600080fd5b813560206147cb6147c68361474f565b614700565b82815260059290921b840181019181810190868411156147ea57600080fd5b8286015b8481101561480e57803561480181614773565b83529183019183016147ee565b509695505050505050565b60ff8116811461109c57600080fd5b80356147a081614819565b63ffffffff8116811461109c57600080fd5b80356147a081614833565b801515811461109c57600080fd5b80356147a081614850565b62ffffff8116811461109c57600080fd5b80356147a081614869565b61ffff8116811461109c57600080fd5b80356147a081614885565b600061020082840312156148b357600080fd5b6148bb6146b3565b90506148c682614845565b81526148d460208301614845565b60208201526148e560408301614845565b60408201526148f660608301614795565b60608201526149076080830161485e565b608082015261491860a0830161487a565b60a082015261492960c08301614845565b60c082015261493a60e08301614845565b60e082015261010061494d818401614795565b9082015261012061495f838201614895565b90820152610140614971838201614795565b90820152610160828101359082015261018080830135908201526101a080830135908201526101c08083013567ffffffffffffffff8111156149b257600080fd5b6149be858286016147a5565b8284015250506101e06149d2818401614795565b9082015292915050565b803567ffffffffffffffff811681146147a057600080fd5b600082601f830112614a0557600080fd5b813567ffffffffffffffff811115614a1f57614a1f614684565b614a5060207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f84011601614700565b818152846020838601011115614a6557600080fd5b816020850160208301376000918101602001919091529392505050565b6bffffffffffffffffffffffff8116811461109c57600080fd5b600082601f830112614aad57600080fd5b81356020614abd6147c68361474f565b82815260c09283028501820192828201919087851115614adc57600080fd5b8387015b85811015614b6e5781818a031215614af85760008081fd5b614b006146dd565b8135614b0b81614833565b815281860135614b1a81614869565b81870152604082810135614b2d81614773565b90820152606082810135614b4081614819565b908201526080828101359082015260a080830135614b5d81614a82565b908201528452928401928101614ae0565b5090979650505050505050565b600080600080600080600080610100898b031215614b9857600080fd5b883567ffffffffffffffff80821115614bb057600080fd5b614bbc8c838d016147a5565b995060208b0135915080821115614bd257600080fd5b614bde8c838d016147a5565b9850614bec60408c01614828565b975060608b0135915080821115614c0257600080fd5b614c0e8c838d016148a0565b9650614c1c60808c016149dc565b955060a08b0135915080821115614c3257600080fd5b614c3e8c838d016149f4565b945060c08b0135915080821115614c5457600080fd5b614c608c838d016147a5565b935060e08b0135915080821115614c7657600080fd5b50614c838b828c01614a9c565b9150509295985092959890939650565b6000815180845260005b81811015614cb957602081850181015186830182015201614c9d565b5060006020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b602081526000611edf6020830184614c93565b60008083601f840112614d1c57600080fd5b50813567ffffffffffffffff811115614d3457600080fd5b6020830191508360208260051b8501011115614d4f57600080fd5b9250929050565b60008060008060008060008060e0898b031215614d7257600080fd5b606089018a811115614d8357600080fd5b8998503567ffffffffffffffff80821115614d9d57600080fd5b818b0191508b601f830112614db157600080fd5b813581811115614dc057600080fd5b8c6020828501011115614dd257600080fd5b6020830199508098505060808b0135915080821115614df057600080fd5b614dfc8c838d01614d0a565b909750955060a08b0135915080821115614e1557600080fd5b50614e228b828c01614d0a565b999c989b50969995989497949560c00135949350505050565b60008060008060008060c08789031215614e5457600080fd5b863567ffffffffffffffff80821115614e6c57600080fd5b614e788a838b016147a5565b97506020890135915080821115614e8e57600080fd5b614e9a8a838b016147a5565b9650614ea860408a01614828565b95506060890135915080821115614ebe57600080fd5b614eca8a838b016149f4565b9450614ed860808a016149dc565b935060a0890135915080821115614eee57600080fd5b50614efb89828a016149f4565b9150509295509295509295565b600060208284031215614f1a57600080fd5b8135611edf81614773565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60ff818116838216029081169081811461325357613253614f25565b600060208284031215614f8257600080fd5b5051919050565b63ffffffff81811683821601908082111561325357613253614f25565b600081518084526020808501945080840160005b83811015614fec57815173ffffffffffffffffffffffffffffffffffffffff1687529582019590820190600101614fba565b509495945050505050565b6020815261500e60208201835163ffffffff169052565b60006020830151615027604084018263ffffffff169052565b50604083015163ffffffff8116606084015250606083015173ffffffffffffffffffffffffffffffffffffffff8116608084015250608083015180151560a08401525060a083015162ffffff811660c08401525060c083015163ffffffff811660e08401525060e08301516101006150a68185018363ffffffff169052565b84015190506101206150cf8482018373ffffffffffffffffffffffffffffffffffffffff169052565b84015190506101406150e68482018361ffff169052565b840151905061016061510f8482018373ffffffffffffffffffffffffffffffffffffffff169052565b840151610180848101919091528401516101a0808501919091528401516101c0808501919091528401516102006101e080860182905291925090615157610220860184614fa6565b95015173ffffffffffffffffffffffffffffffffffffffff169301929092525090919050565b81810381811115611ecd57611ecd614f25565b60008161519f5761519f614f25565b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361522557615225614f25565b5060010190565b600061012063ffffffff808d1684528b6020850152808b1660408501525080606084015261525c8184018a614fa6565b905082810360808401526152708189614fa6565b905060ff871660a084015282810360c084015261528d8187614c93565b905067ffffffffffffffff851660e08401528281036101008401526152b28185614c93565b9c9b505050505050505050505050565b8082028115828204841417611ecd57611ecd614f25565b80820180821115611ecd57611ecd614f25565b60ff8181168382160190811115611ecd57611ecd614f25565b80516147a081614833565b80516147a081614773565b80516147a081614850565b80516147a081614869565b80516147a081614885565b600082601f83011261534d57600080fd5b8151602061535d6147c68361474f565b82815260059290921b8401810191818101908684111561537c57600080fd5b8286015b8481101561480e57805161539381614773565b8352918301918301615380565b600082601f8301126153b157600080fd5b815160206153c16147c68361474f565b82815260c092830285018201928282019190878511156153e057600080fd5b8387015b85811015614b6e5781818a0312156153fc5760008081fd5b6154046146dd565b815161540f81614833565b81528186015161541e81614869565b8187015260408281015161543181614773565b9082015260608281015161544481614819565b908201526080828101519082015260a08083015161546181614a82565b9082015284529284019281016153e4565b60008060006060848603121561548757600080fd5b835167ffffffffffffffff8082111561549f57600080fd5b9085019061020082880312156154b457600080fd5b6154bc6146b3565b6154c583615305565b81526154d360208401615305565b60208201526154e460408401615305565b60408201526154f560608401615310565b60608201526155066080840161531b565b608082015261551760a08401615326565b60a082015261552860c08401615305565b60c082015261553960e08401615305565b60e082015261010061554c818501615310565b9082015261012061555e848201615331565b90820152610140615570848201615310565b90820152610160838101519082015261018080840151908201526101a080840151908201526101c080840151838111156155a957600080fd5b6155b58a82870161533c565b8284015250506101e06155c9818501615310565b9082015260208701519095509150808211156155e457600080fd5b6155f08783880161533c565b9350604086015191508082111561560657600080fd5b50615613868287016153a0565b9150509250925092565b60006020828403121561562f57600080fd5b8151611edf81614819565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b60006101208b835273ffffffffffffffffffffffffffffffffffffffff8b16602084015267ffffffffffffffff808b1660408501528160608501526156b08285018b614fa6565b915083820360808501526156c4828a614fa6565b915060ff881660a085015283820360c08501526156e18288614c93565b90861660e085015283810361010085015290506152b28185614c93565b8183823760009101908152919050565b8281526080810160608360208401379392505050565b600082601f83011261573557600080fd5b813560206157456147c68361474f565b82815260059290921b8401810191818101908684111561576457600080fd5b8286015b8481101561480e5780358352918301918301615768565b600082601f83011261579057600080fd5b813560206157a06147c68361474f565b82815260059290921b840181019181810190868411156157bf57600080fd5b8286015b8481101561480e57803567ffffffffffffffff8111156157e35760008081fd5b6157f18986838b01016149f4565b8452509183019183016157c3565b60006020828403121561581157600080fd5b813567ffffffffffffffff8082111561582957600080fd5b9083019060c0828603121561583d57600080fd5b6158456146dd565b823581526020830135602082015260408301358281111561586557600080fd5b61587187828601615724565b60408301525060608301358281111561588957600080fd5b61589587828601615724565b6060830152506080830135828111156158ad57600080fd5b6158b98782860161577f565b60808301525060a0830135828111156158d157600080fd5b6158dd8782860161577f565b60a08301525095945050505050565b61ffff81811683821601908082111561325357613253614f25565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b60008261594557615945615907565b500490565b6bffffffffffffffffffffffff81811683821601908082111561325357613253614f25565b6bffffffffffffffffffffffff851681528360208201528260408201526080606082015260006159a26080830184614c93565b9695505050505050565b6bffffffffffffffffffffffff82811682821603908082111561325357613253614f25565b60006bffffffffffffffffffffffff808416806159f0576159f0615907565b92169190910492915050565b6bffffffffffffffffffffffff818116838216028082169190828114615a2457615a24614f25565b505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b82815260406020820152600061334c6040830184614c93565b60008060408385031215615a8757600080fd5b8251615a9281614850565b6020939093015192949293505050565b805169ffffffffffffffffffff811681146147a057600080fd5b600080600080600060a08688031215615ad457600080fd5b615add86615aa2565b9450602086015193506040860151925060608601519150615b0060808701615aa2565b90509295509295909350565b600181815b80851115615b6557817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115615b4b57615b4b614f25565b80851615615b5857918102915b93841c9390800290615b11565b509250929050565b600082615b7c57506001611ecd565b81615b8957506000611ecd565b8160018114615b9f5760028114615ba957615bc5565b6001915050611ecd565b60ff841115615bba57615bba614f25565b50506001821b611ecd565b5060208310610133831016604e8410600b8410161715615be8575081810a611ecd565b615bf28383615b0c565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115615c2457615c24614f25565b029392505050565b6000611edf8383615b6d565b6fffffffffffffffffffffffffffffffff81811683821601908082111561325357613253614f25565b6000610100820190506bffffffffffffffffffffffff8084511683528060208501511660208401528060408501511660408401528060608501511660608401525073ffffffffffffffffffffffffffffffffffffffff608084015116608083015260a0830151615ce160a08401826bffffffffffffffffffffffff169052565b5060c0830151615d0160c08401826bffffffffffffffffffffffff169052565b5060e083015161325360e08401826bffffffffffffffffffffffff169052565b600060408284031215615d3357600080fd5b6040516040810181811067ffffffffffffffff82111715615d5657615d56614684565b6040528251615d6481614833565b81526020928301519281019290925250919050565b600060a08284031215615d8b57600080fd5b60405160a0810181811067ffffffffffffffff82111715615dae57615dae614684565b806040525082518152602083015160208201526040830151615dcf81614833565b60408201526060830151615de281614833565b6060820152608092830151928101929092525091905056fea164736f6c6343000813000a", } var AutomationRegistryABI = AutomationRegistryMetaData.ABI diff --git a/core/gethwrappers/generation/generated-wrapper-dependency-versions-do-not-edit.txt b/core/gethwrappers/generation/generated-wrapper-dependency-versions-do-not-edit.txt index 67c6ff7ae7f..e8913329364 100644 --- a/core/gethwrappers/generation/generated-wrapper-dependency-versions-do-not-edit.txt +++ b/core/gethwrappers/generation/generated-wrapper-dependency-versions-do-not-edit.txt @@ -13,8 +13,9 @@ automation_registry_logic_a_wrapper_2_2: ../../contracts/solc/v0.8.19/Automation automation_registry_logic_a_wrapper_2_3: ../../contracts/solc/v0.8.19/AutomationRegistryLogicA2_3/AutomationRegistryLogicA2_3.abi ../../contracts/solc/v0.8.19/AutomationRegistryLogicA2_3/AutomationRegistryLogicA2_3.bin 73b5cc3ece642abbf6f2a4c9188335b71404f4dd0ad10b761390b6397af6f1c8 automation_registry_logic_b_wrapper_2_2: ../../contracts/solc/v0.8.19/AutomationRegistryLogicB2_2/AutomationRegistryLogicB2_2.abi ../../contracts/solc/v0.8.19/AutomationRegistryLogicB2_2/AutomationRegistryLogicB2_2.bin a6d33dfbbfb0ff253eb59a51f4f6d6d4c22ea5ec95aae52d25d49a312b37a22f automation_registry_logic_b_wrapper_2_3: ../../contracts/solc/v0.8.19/AutomationRegistryLogicB2_3/AutomationRegistryLogicB2_3.abi ../../contracts/solc/v0.8.19/AutomationRegistryLogicB2_3/AutomationRegistryLogicB2_3.bin fbf6f6cf4e6858855ff5da847c3baa4859dd997cfae51f2fa0651e4fa15b92c9 +automation_registry_logic_c_wrapper_2_3: ../../contracts/solc/v0.8.19/AutomationRegistryLogicC2_3/AutomationRegistryLogicC2_3.abi ../../contracts/solc/v0.8.19/AutomationRegistryLogicC2_3/AutomationRegistryLogicC2_3.bin 3ee51aa2f946b9fe3583b4a8526d29721339f96774e410bd37ddfe8184a63701 automation_registry_wrapper_2_2: ../../contracts/solc/v0.8.19/AutomationRegistry2_2/AutomationRegistry2_2.abi ../../contracts/solc/v0.8.19/AutomationRegistry2_2/AutomationRegistry2_2.bin de60f69878e9b32a291a001c91fc8636544c2cfbd9b507c8c1a4873b602bfb62 -automation_registry_wrapper_2_3: ../../contracts/solc/v0.8.19/AutomationRegistry2_3/AutomationRegistry2_3.abi ../../contracts/solc/v0.8.19/AutomationRegistry2_3/AutomationRegistry2_3.bin 10078161924b38cf968ceb65f54078412832ada9abeebcd011ee7291811921c2 +automation_registry_wrapper_2_3: ../../contracts/solc/v0.8.19/AutomationRegistry2_3/AutomationRegistry2_3.abi ../../contracts/solc/v0.8.19/AutomationRegistry2_3/AutomationRegistry2_3.bin f8f920a225fdb1e36948dd95bae3aa46ecc2b01fd113480e111960b5e5f95624 automation_utils_2_1: ../../contracts/solc/v0.8.16/AutomationUtils2_1/AutomationUtils2_1.abi ../../contracts/solc/v0.8.16/AutomationUtils2_1/AutomationUtils2_1.bin 815b17b63f15d26a0274b962eefad98cdee4ec897ead58688bbb8e2470e585f5 automation_utils_2_2: ../../contracts/solc/v0.8.19/AutomationUtils2_2/AutomationUtils2_2.abi ../../contracts/solc/v0.8.19/AutomationUtils2_2/AutomationUtils2_2.bin 8743f6231aaefa3f2a0b2d484258070d506e2d0860690e66890dccc3949edb2e automation_utils_2_3: ../../contracts/solc/v0.8.19/AutomationUtils2_3/AutomationUtils2_3.abi ../../contracts/solc/v0.8.19/AutomationUtils2_3/AutomationUtils2_3.bin 11e2b481dc9a4d936e3443345d45d2cc571164459d214917b42a8054b295393b diff --git a/core/gethwrappers/go_generate.go b/core/gethwrappers/go_generate.go index 5803e67c17b..1ab61563bf7 100644 --- a/core/gethwrappers/go_generate.go +++ b/core/gethwrappers/go_generate.go @@ -48,6 +48,7 @@ package gethwrappers //go:generate go run ./generation/generate/wrap.go ../../contracts/solc/v0.8.19/AutomationRegistry2_3/AutomationRegistry2_3.abi ../../contracts/solc/v0.8.19/AutomationRegistry2_3/AutomationRegistry2_3.bin AutomationRegistry automation_registry_wrapper_2_3 //go:generate go run ./generation/generate/wrap.go ../../contracts/solc/v0.8.19/AutomationRegistryLogicA2_3/AutomationRegistryLogicA2_3.abi ../../contracts/solc/v0.8.19/AutomationRegistryLogicA2_3/AutomationRegistryLogicA2_3.bin AutomationRegistryLogicA automation_registry_logic_a_wrapper_2_3 //go:generate go run ./generation/generate/wrap.go ../../contracts/solc/v0.8.19/AutomationRegistryLogicB2_3/AutomationRegistryLogicB2_3.abi ../../contracts/solc/v0.8.19/AutomationRegistryLogicB2_3/AutomationRegistryLogicB2_3.bin AutomationRegistryLogicB automation_registry_logic_b_wrapper_2_3 +//go:generate go run ./generation/generate/wrap.go ../../contracts/solc/v0.8.19/AutomationRegistryLogicC2_3/AutomationRegistryLogicC2_3.abi ../../contracts/solc/v0.8.19/AutomationRegistryLogicC2_3/AutomationRegistryLogicC2_3.bin AutomationRegistryLogicC automation_registry_logic_c_wrapper_2_3 //go:generate go run ./generation/generate/wrap.go ../../contracts/solc/v0.8.19/IAutomationRegistryMaster2_3/IAutomationRegistryMaster2_3.abi ../../contracts/solc/v0.8.19/IAutomationRegistryMaster2_3/IAutomationRegistryMaster2_3.bin IAutomationRegistryMaster2_3 i_automation_registry_master_wrapper_2_3 //go:generate go run ./generation/generate/wrap.go ../../contracts/solc/v0.8.19/AutomationUtils2_3/AutomationUtils2_3.abi ../../contracts/solc/v0.8.19/AutomationUtils2_3/AutomationUtils2_3.bin AutomationUtils automation_utils_2_3 //go:generate go run ./generation/generate/wrap.go ../../contracts/solc/v0.8.19/ArbitrumModule/ArbitrumModule.abi ../../contracts/solc/v0.8.19/ArbitrumModule/ArbitrumModule.bin ArbitrumModule arbitrum_module diff --git a/core/gethwrappers/keystone/generated/forwarder/forwarder.go b/core/gethwrappers/keystone/generated/forwarder/forwarder.go index fbdd024c64f..951ef047364 100644 --- a/core/gethwrappers/keystone/generated/forwarder/forwarder.go +++ b/core/gethwrappers/keystone/generated/forwarder/forwarder.go @@ -31,8 +31,8 @@ var ( ) var KeystoneForwarderMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"signer\",\"type\":\"address\"}],\"name\":\"DuplicateSigner\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"numSigners\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxSigners\",\"type\":\"uint256\"}],\"name\":\"ExcessSigners\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FaultToleranceMustBePositive\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"numSigners\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"minSigners\",\"type\":\"uint256\"}],\"name\":\"InsufficientSigners\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"}],\"name\":\"InvalidDonId\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidReport\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"InvalidSignature\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"expected\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"received\",\"type\":\"uint256\"}],\"name\":\"InvalidSignatureCount\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"signer\",\"type\":\"address\"}],\"name\":\"InvalidSigner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ReentrantCall\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"reportId\",\"type\":\"bytes32\"}],\"name\":\"ReportAlreadyProcessed\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"workflowOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"result\",\"type\":\"bool\"}],\"name\":\"ReportProcessed\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"acceptOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"}],\"name\":\"getTransmitter\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiverAddress\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"rawReport\",\"type\":\"bytes\"},{\"internalType\":\"bytes[]\",\"name\":\"signatures\",\"type\":\"bytes[]\"}],\"name\":\"report\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"internalType\":\"address[]\",\"name\":\"signers\",\"type\":\"address[]\"}],\"name\":\"setConfig\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"typeAndVersion\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}]", - Bin: "0x608060405234801561001057600080fd5b5033806000816100675760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b0384811691909117909155811615610097576100978161009f565b505050610148565b336001600160a01b038216036100f75760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c66000000000000000000604482015260640161005e565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b61151d806101576000396000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c806379ba50971161005b57806379ba5097146101175780638da5cb5b1461011f578063c0965dc31461013d578063f2fde38b1461015057600080fd5b8063134a46f014610082578063181f5a7714610097578063390d0b15146100df575b600080fd5b610095610090366004611106565b610163565b005b604080518082018252601781527f4b657973746f6e65466f7277617264657220312e302e30000000000000000000602082015290516100d691906111de565b60405180910390f35b6100f26100ed366004611221565b61057d565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100d6565b61009561060e565b60005473ffffffffffffffffffffffffffffffffffffffff166100f2565b61009561014b36600461124b565b61070b565b61009561015e3660046112fa565b610ded565b60015474010000000000000000000000000000000000000000900460ff16156101b8576040517f37ed32e800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff167401000000000000000000000000000000000000000017905560ff8316600003610234576040517f0743bae600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b601f81111561027e576040517f61750f4000000000000000000000000000000000000000000000000000000000815260048101829052601f60248201526044015b60405180910390fd5b610289836003611344565b60ff1681116102e7578061029e846003611344565b6102a9906001611367565b6040517f9dd9e6d8000000000000000000000000000000000000000000000000000000008152600481019290925260ff166024820152604401610275565b60005b63ffffffff85166000908152600260205260409020600101548110156103885763ffffffff8516600090815260026020526040812060010180548390811061033457610334611380565b600091825260208083209091015463ffffffff891683526002808352604080852073ffffffffffffffffffffffffffffffffffffffff9093168552910190915281205550610381816113af565b90506102ea565b5063ffffffff841660009081526002602052604090206103ac906001018383610ffe565b5060005b8181101561050a5760008383838181106103cc576103cc611380565b90506020020160208101906103e191906112fa565b63ffffffff8716600090815260026020818152604080842073ffffffffffffffffffffffffffffffffffffffff8616855290920190529020549091501561046c576040517fe021c4f200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82166004820152602401610275565b610477826001611367565b63ffffffff8716600090815260026020818152604080842073ffffffffffffffffffffffffffffffffffffffff909616808552868401835290842060ff959095169094559081526001938401805494850181558252902090910180547fffffffffffffffffffffffff0000000000000000000000000000000000000000169091179055610503816113af565b90506103b0565b50505063ffffffff91909116600090815260026020526040902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff909216919091179055600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff169055565b6000806105df84846040517fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606084901b1660208201526034810182905260009060540160405160208183030381529060405280519060200120905092915050565b60009081526003602052604090205473ffffffffffffffffffffffffffffffffffffffff169150505b92915050565b60015473ffffffffffffffffffffffffffffffffffffffff16331461068f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e6572000000000000000000006044820152606401610275565b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b60015474010000000000000000000000000000000000000000900460ff1615610760576040517f37ed32e800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff167401000000000000000000000000000000000000000017905560588310156107da576040517fb55ac75400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008060008061081f88888080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610e0192505050565b63ffffffff8316600090815260026020526040812054949850929650909450925060ff9091169003610885576040517fea1b312900000000000000000000000000000000000000000000000000000000815263ffffffff84166004820152602401610275565b604080517fffffffffffffffffffffffffffffffffffffffff00000000000000000000000060608c901b1660208083019190915260348083018690528351808403909101815260549092018352815191810191909120600081815260039092529190205473ffffffffffffffffffffffffffffffffffffffff1615610939576040517f1aac3d2900000000000000000000000000000000000000000000000000000000815260048101829052602401610275565b63ffffffff8416600090815260026020526040902054869061095f9060ff166001611367565b60ff16146109ca5763ffffffff841660009081526002602052604090205461098b9060ff166001611367565b6040517fd6022e8e00000000000000000000000000000000000000000000000000000000815260ff909116600482015260248101879052604401610275565b600089896040516109dc9291906113e7565b604051809103902090506109ee611086565b6000805b89811015610c5c576000806000610a608e8e86818110610a1457610a14611380565b9050602002810190610a2691906113f7565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610e2692505050565b925092509250600060018883868660405160008152602001604052604051610aa4949392919093845260ff9290921660208401526040830152606082015260800190565b6020604051602081039080840390855afa158015610ac6573d6000803e3d6000fd5b5050604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015163ffffffff8f1660009081526002602081815284832073ffffffffffffffffffffffffffffffffffffffff851684529091019052918220549850925060ff881690039050610b81576040517fbf18af4300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82166004820152602401610275565b610b8c60018761145c565b955060008760ff8816601f8110610ba557610ba5611380565b602002015173ffffffffffffffffffffffffffffffffffffffff1614610c0f576040517fe021c4f200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82166004820152602401610275565b80878760ff16601f8110610c2557610c25611380565b73ffffffffffffffffffffffffffffffffffffffff909216602092909202015250610c5592508391506113af9050565b90506109f2565b5050505060008a905060008173ffffffffffffffffffffffffffffffffffffffff1663ff5a027088868e8e6058908092610c9893929190611475565b6040518563ffffffff1660e01b8152600401610cb7949392919061149f565b600060405180830381600087803b158015610cd157600080fd5b505af1925050508015610ce2575060015b15610ceb575060015b604080518082018252338152821515602080830191825260008781526003909152839020915182549151151574010000000000000000000000000000000000000000027fffffffffffffffffffffff00000000000000000000000000000000000000000090921673ffffffffffffffffffffffffffffffffffffffff91821617919091179091559051869186811691908f16907fdae8e752043eb5fc7e4a6eced57ceaf159548b630125ece9ffc41cfc952c208190610daf90861515815260200190565b60405180910390a45050600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16905550505050505050505050565b610df5610e86565b610dfe81610f09565b50565b602081015160408201516044830151606490930151919360e09190911c929160601c90565b60008060006041845114610e6857836040517f2adfdc3000000000000000000000000000000000000000000000000000000000815260040161027591906111de565b50505060208101516040820151606090920151909260009190911a90565b60005473ffffffffffffffffffffffffffffffffffffffff163314610f07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e6572000000000000000000006044820152606401610275565b565b3373ffffffffffffffffffffffffffffffffffffffff821603610f88576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c660000000000000000006044820152606401610275565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b828054828255906000526020600020908101928215611076579160200282015b828111156110765781547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff84351617825560209092019160019091019061101e565b506110829291506110a5565b5090565b604051806103e00160405280601f906020820280368337509192915050565b5b8082111561108257600081556001016110a6565b60008083601f8401126110cc57600080fd5b50813567ffffffffffffffff8111156110e457600080fd5b6020830191508360208260051b85010111156110ff57600080fd5b9250929050565b6000806000806060858703121561111c57600080fd5b843563ffffffff8116811461113057600080fd5b9350602085013560ff8116811461114657600080fd5b9250604085013567ffffffffffffffff81111561116257600080fd5b61116e878288016110ba565b95989497509550505050565b6000815180845260005b818110156111a057602081850181015186830182015201611184565b5060006020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b6020815260006111f1602083018461117a565b9392505050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461121c57600080fd5b919050565b6000806040838503121561123457600080fd5b61123d836111f8565b946020939093013593505050565b60008060008060006060868803121561126357600080fd5b61126c866111f8565b9450602086013567ffffffffffffffff8082111561128957600080fd5b818801915088601f83011261129d57600080fd5b8135818111156112ac57600080fd5b8960208285010111156112be57600080fd5b6020830196508095505060408801359150808211156112dc57600080fd5b506112e9888289016110ba565b969995985093965092949392505050565b60006020828403121561130c57600080fd5b6111f1826111f8565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60ff818116838216029081169081811461136057611360611315565b5092915050565b60ff818116838216019081111561060857610608611315565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036113e0576113e0611315565b5060010190565b8183823760009101908152919050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe184360301811261142c57600080fd5b83018035915067ffffffffffffffff82111561144757600080fd5b6020019150368190038213156110ff57600080fd5b60ff828116828216039081111561060857610608611315565b6000808585111561148557600080fd5b8386111561149257600080fd5b5050820193919092039150565b84815273ffffffffffffffffffffffffffffffffffffffff8416602082015260606040820152816060820152818360808301376000818301608090810191909152601f9092017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0160101939250505056fea164736f6c6343000813000a", + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"signer\",\"type\":\"address\"}],\"name\":\"DuplicateSigner\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"numSigners\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxSigners\",\"type\":\"uint256\"}],\"name\":\"ExcessSigners\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FaultToleranceMustBePositive\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"numSigners\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"minSigners\",\"type\":\"uint256\"}],\"name\":\"InsufficientSigners\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"}],\"name\":\"InvalidDonId\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidReport\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"InvalidSignature\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"expected\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"received\",\"type\":\"uint256\"}],\"name\":\"InvalidSignatureCount\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"signer\",\"type\":\"address\"}],\"name\":\"InvalidSigner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ReentrantCall\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"reportId\",\"type\":\"bytes32\"}],\"name\":\"ReportAlreadyProcessed\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"workflowOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"result\",\"type\":\"bool\"}],\"name\":\"ReportProcessed\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"acceptOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"}],\"name\":\"getTransmitter\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"receiverAddress\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"rawReport\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"reportContext\",\"type\":\"bytes\"},{\"internalType\":\"bytes[]\",\"name\":\"signatures\",\"type\":\"bytes[]\"}],\"name\":\"report\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"donId\",\"type\":\"uint32\"},{\"internalType\":\"uint8\",\"name\":\"f\",\"type\":\"uint8\"},{\"internalType\":\"address[]\",\"name\":\"signers\",\"type\":\"address[]\"}],\"name\":\"setConfig\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"typeAndVersion\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}]", + Bin: "0x608060405234801561001057600080fd5b5033806000816100675760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b0384811691909117909155811615610097576100978161009f565b505050610148565b336001600160a01b038216036100f75760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c66000000000000000000604482015260640161005e565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b61152c806101576000396000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c8063390d0b151161005b578063390d0b15146100f257806379ba50971461012a5780638da5cb5b14610132578063f2fde38b1461015057600080fd5b80631128956514610082578063134a46f014610097578063181f5a77146100aa575b600080fd5b610095610090366004611128565b610163565b005b6100956100a53660046111d3565b6108b5565b604080518082018252601781527f4b657973746f6e65466f7277617264657220312e302e30000000000000000000602082015290516100e991906112ab565b60405180910390f35b6101056101003660046112c5565b610c16565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100e9565b610095610ca7565b60005473ffffffffffffffffffffffffffffffffffffffff16610105565b61009561015e3660046112ef565b610da4565b60015474010000000000000000000000000000000000000000900460ff16156101b8576040517f37ed32e800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16740100000000000000000000000000000000000000001790556058851015610232576040517fb55ac75400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6000806000806102778a8a8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610db892505050565b63ffffffff8316600090815260026020526040812054949850929650909450925060ff90911690036102e2576040517fea1b312900000000000000000000000000000000000000000000000000000000815263ffffffff841660048201526024015b60405180910390fd5b604080517fffffffffffffffffffffffffffffffffffffffff00000000000000000000000060608e901b1660208083019190915260348083018690528351808403909101815260549092018352815191810191909120600081815260039092529190205473ffffffffffffffffffffffffffffffffffffffff1615610396576040517f1aac3d29000000000000000000000000000000000000000000000000000000008152600481018290526024016102d9565b63ffffffff841660009081526002602052604090205486906103bc9060ff166001611339565b60ff16146104275763ffffffff84166000908152600260205260409020546103e89060ff166001611339565b6040517fd6022e8e00000000000000000000000000000000000000000000000000000000815260ff9091166004820152602481018790526044016102d9565b60008b8b604051610439929190611352565b604051908190038120610452918c908c90602001611362565b604051602081830303815290604052805190602001209050610472610fb5565b6000805b898110156106e05760008060006104e48e8e868181106104985761049861137c565b90506020028101906104aa91906113ab565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610ddd92505050565b9194509250905060006001886104fb84601b611339565b6040805160008152602081018083529390935260ff90911690820152606081018690526080810185905260a0016020604051602081039080840390855afa15801561054a573d6000803e3d6000fd5b5050604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015163ffffffff8f1660009081526002602081815284832073ffffffffffffffffffffffffffffffffffffffff851684529091019052918220549850925060ff881690039050610605576040517fbf18af4300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff821660048201526024016102d9565b610610600187611410565b955060008760ff8816601f81106106295761062961137c565b602002015173ffffffffffffffffffffffffffffffffffffffff1614610693576040517fe021c4f200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff821660048201526024016102d9565b80878760ff16601f81106106a9576106a961137c565b73ffffffffffffffffffffffffffffffffffffffff9092166020929092020152506106d992508391506114299050565b9050610476565b5050505060008c73ffffffffffffffffffffffffffffffffffffffff1663ff5a027087858f8f605890809261071793929190611461565b6040518563ffffffff1660e01b8152600401610736949392919061148b565b600060405180830381600087803b15801561075057600080fd5b505af1925050508015610761575060015b1561076a575060015b60405180604001604052803373ffffffffffffffffffffffffffffffffffffffff1681526020018215158152506003600084815260200190815260200160002060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060208201518160000160146101000a81548160ff021916908315150217905550905050838373ffffffffffffffffffffffffffffffffffffffff168e73ffffffffffffffffffffffffffffffffffffffff167fdae8e752043eb5fc7e4a6eced57ceaf159548b630125ece9ffc41cfc952c208184604051610876911515815260200190565b60405180910390a45050600180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690555050505050505050505050565b6108bd610e3d565b8260ff166000036108fa576040517f0743bae600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b601f81111561093f576040517f61750f4000000000000000000000000000000000000000000000000000000000815260048101829052601f60248201526044016102d9565b61094a8360036114fc565b60ff1681116109a8578061095f8460036114fc565b61096a906001611339565b6040517f9dd9e6d8000000000000000000000000000000000000000000000000000000008152600481019290925260ff1660248201526044016102d9565b60005b63ffffffff8516600090815260026020526040902060010154811015610a495763ffffffff851660009081526002602052604081206001018054839081106109f5576109f561137c565b600091825260208083209091015463ffffffff891683526002808352604080852073ffffffffffffffffffffffffffffffffffffffff9093168552910190915281205550610a4281611429565b90506109ab565b5063ffffffff84166000908152600260205260409020610a6d906001018383610fd4565b5060005b81811015610bcb576000838383818110610a8d57610a8d61137c565b9050602002016020810190610aa291906112ef565b63ffffffff8716600090815260026020818152604080842073ffffffffffffffffffffffffffffffffffffffff86168552909201905290205490915015610b2d576040517fe021c4f200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff821660048201526024016102d9565b610b38826001611339565b63ffffffff8716600090815260026020818152604080842073ffffffffffffffffffffffffffffffffffffffff909616808552868401835290842060ff959095169094559081526001938401805494850181558252902090910180547fffffffffffffffffffffffff0000000000000000000000000000000000000000169091179055610bc481611429565b9050610a71565b50505063ffffffff91909116600090815260026020526040902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff909216919091179055565b600080610c7884846040517fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606084901b1660208201526034810182905260009060540160405160208183030381529060405280519060200120905092915050565b60009081526003602052604090205473ffffffffffffffffffffffffffffffffffffffff169150505b92915050565b60015473ffffffffffffffffffffffffffffffffffffffff163314610d28576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e65720000000000000000000060448201526064016102d9565b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b610dac610e3d565b610db581610ec0565b50565b602081015160408201516044830151606490930151919360e09190911c929160601c90565b60008060006041845114610e1f57836040517f2adfdc300000000000000000000000000000000000000000000000000000000081526004016102d991906112ab565b50505060208101516040820151606090920151909260009190911a90565b60005473ffffffffffffffffffffffffffffffffffffffff163314610ebe576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e65720000000000000000000060448201526064016102d9565b565b3373ffffffffffffffffffffffffffffffffffffffff821603610f3f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c6600000000000000000060448201526064016102d9565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b604051806103e00160405280601f906020820280368337509192915050565b82805482825590600052602060002090810192821561104c579160200282015b8281111561104c5781547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff843516178255602090920191600190910190610ff4565b5061105892915061105c565b5090565b5b80821115611058576000815560010161105d565b803573ffffffffffffffffffffffffffffffffffffffff8116811461109557600080fd5b919050565b60008083601f8401126110ac57600080fd5b50813567ffffffffffffffff8111156110c457600080fd5b6020830191508360208285010111156110dc57600080fd5b9250929050565b60008083601f8401126110f557600080fd5b50813567ffffffffffffffff81111561110d57600080fd5b6020830191508360208260051b85010111156110dc57600080fd5b60008060008060008060006080888a03121561114357600080fd5b61114c88611071565b9650602088013567ffffffffffffffff8082111561116957600080fd5b6111758b838c0161109a565b909850965060408a013591508082111561118e57600080fd5b61119a8b838c0161109a565b909650945060608a01359150808211156111b357600080fd5b506111c08a828b016110e3565b989b979a50959850939692959293505050565b600080600080606085870312156111e957600080fd5b843563ffffffff811681146111fd57600080fd5b9350602085013560ff8116811461121357600080fd5b9250604085013567ffffffffffffffff81111561122f57600080fd5b61123b878288016110e3565b95989497509550505050565b6000815180845260005b8181101561126d57602081850181015186830182015201611251565b5060006020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b6020815260006112be6020830184611247565b9392505050565b600080604083850312156112d857600080fd5b6112e183611071565b946020939093013593505050565b60006020828403121561130157600080fd5b6112be82611071565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60ff8181168382160190811115610ca157610ca161130a565b8183823760009101908152919050565b838152818360208301376000910160200190815292915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18436030181126113e057600080fd5b83018035915067ffffffffffffffff8211156113fb57600080fd5b6020019150368190038213156110dc57600080fd5b60ff8281168282160390811115610ca157610ca161130a565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361145a5761145a61130a565b5060010190565b6000808585111561147157600080fd5b8386111561147e57600080fd5b5050820193919092039150565b84815273ffffffffffffffffffffffffffffffffffffffff8416602082015260606040820152816060820152818360808301376000818301608090810191909152601f9092017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01601019392505050565b60ff81811683821602908116908181146115185761151861130a565b509291505056fea164736f6c6343000813000a", } var KeystoneForwarderABI = KeystoneForwarderMetaData.ABI @@ -249,16 +249,16 @@ func (_KeystoneForwarder *KeystoneForwarderTransactorSession) AcceptOwnership() return _KeystoneForwarder.Contract.AcceptOwnership(&_KeystoneForwarder.TransactOpts) } -func (_KeystoneForwarder *KeystoneForwarderTransactor) Report(opts *bind.TransactOpts, receiverAddress common.Address, rawReport []byte, signatures [][]byte) (*types.Transaction, error) { - return _KeystoneForwarder.contract.Transact(opts, "report", receiverAddress, rawReport, signatures) +func (_KeystoneForwarder *KeystoneForwarderTransactor) Report(opts *bind.TransactOpts, receiverAddress common.Address, rawReport []byte, reportContext []byte, signatures [][]byte) (*types.Transaction, error) { + return _KeystoneForwarder.contract.Transact(opts, "report", receiverAddress, rawReport, reportContext, signatures) } -func (_KeystoneForwarder *KeystoneForwarderSession) Report(receiverAddress common.Address, rawReport []byte, signatures [][]byte) (*types.Transaction, error) { - return _KeystoneForwarder.Contract.Report(&_KeystoneForwarder.TransactOpts, receiverAddress, rawReport, signatures) +func (_KeystoneForwarder *KeystoneForwarderSession) Report(receiverAddress common.Address, rawReport []byte, reportContext []byte, signatures [][]byte) (*types.Transaction, error) { + return _KeystoneForwarder.Contract.Report(&_KeystoneForwarder.TransactOpts, receiverAddress, rawReport, reportContext, signatures) } -func (_KeystoneForwarder *KeystoneForwarderTransactorSession) Report(receiverAddress common.Address, rawReport []byte, signatures [][]byte) (*types.Transaction, error) { - return _KeystoneForwarder.Contract.Report(&_KeystoneForwarder.TransactOpts, receiverAddress, rawReport, signatures) +func (_KeystoneForwarder *KeystoneForwarderTransactorSession) Report(receiverAddress common.Address, rawReport []byte, reportContext []byte, signatures [][]byte) (*types.Transaction, error) { + return _KeystoneForwarder.Contract.Report(&_KeystoneForwarder.TransactOpts, receiverAddress, rawReport, reportContext, signatures) } func (_KeystoneForwarder *KeystoneForwarderTransactor) SetConfig(opts *bind.TransactOpts, donId uint32, f uint8, signers []common.Address) (*types.Transaction, error) { @@ -742,7 +742,7 @@ type KeystoneForwarderInterface interface { AcceptOwnership(opts *bind.TransactOpts) (*types.Transaction, error) - Report(opts *bind.TransactOpts, receiverAddress common.Address, rawReport []byte, signatures [][]byte) (*types.Transaction, error) + Report(opts *bind.TransactOpts, receiverAddress common.Address, rawReport []byte, reportContext []byte, signatures [][]byte) (*types.Transaction, error) SetConfig(opts *bind.TransactOpts, donId uint32, f uint8, signers []common.Address) (*types.Transaction, error) diff --git a/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt b/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt index c58901795ce..bedaa8320eb 100644 --- a/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt +++ b/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt @@ -1,4 +1,4 @@ GETH_VERSION: 1.13.8 -forwarder: ../../../contracts/solc/v0.8.19/KeystoneForwarder/KeystoneForwarder.abi ../../../contracts/solc/v0.8.19/KeystoneForwarder/KeystoneForwarder.bin ed9164cfe4619dff824b11df46b66f4c6834b2ca072923f10d9ebc57ce508ed8 +forwarder: ../../../contracts/solc/v0.8.19/KeystoneForwarder/KeystoneForwarder.abi ../../../contracts/solc/v0.8.19/KeystoneForwarder/KeystoneForwarder.bin 7de386c8c4f6cc82ee5d57c35725c522bc3ee0276356b3dce19e1735e70f17b2 keystone_capability_registry: ../../../contracts/solc/v0.8.19/CapabilityRegistry/CapabilityRegistry.abi ../../../contracts/solc/v0.8.19/CapabilityRegistry/CapabilityRegistry.bin 0a79d0eba13fd4a4b83d7618bb181c21c42222f3cc6c5a90a09302f685555033 ocr3_capability: ../../../contracts/solc/v0.8.19/OCR3Capability/OCR3Capability.abi ../../../contracts/solc/v0.8.19/OCR3Capability/OCR3Capability.bin 9dcbdf55bd5729ba266148da3f17733eb592c871c2108ccca546618628fd9ad2 diff --git a/core/internal/cltest/factories.go b/core/internal/cltest/factories.go index cd2fa9d9f63..c488dca94a9 100644 --- a/core/internal/cltest/factories.go +++ b/core/internal/cltest/factories.go @@ -262,14 +262,15 @@ type RandomKey struct { func (r RandomKey) MustInsert(t testing.TB, keystore keystore.Eth) (ethkey.KeyV2, common.Address) { ctx := testutils.Context(t) - if r.chainIDs == nil { - r.chainIDs = []ubig.Big{*ubig.New(&FixtureChainID)} + chainIDs := r.chainIDs + if chainIDs == nil { + chainIDs = []ubig.Big{*ubig.New(&FixtureChainID)} } key := MustGenerateRandomKey(t) keystore.XXXTestingOnlyAdd(ctx, key) - for _, cid := range r.chainIDs { + for _, cid := range chainIDs { require.NoError(t, keystore.Add(ctx, key.Address, cid.ToInt())) require.NoError(t, keystore.Enable(ctx, key.Address, cid.ToInt())) if r.Disabled { diff --git a/core/internal/features/ocr2/features_ocr2_test.go b/core/internal/features/ocr2/features_ocr2_test.go index 5dc39ccfc7a..440f68d8931 100644 --- a/core/internal/features/ocr2/features_ocr2_test.go +++ b/core/internal/features/ocr2/features_ocr2_test.go @@ -491,7 +491,7 @@ juelsPerFeeCoinSource = """ answer1 [type=median index=0]; """ -gasPriceSource = """ +gasPriceSubunitsSource = """ // data source dsp [type=bridge name="%s"]; dsp_parse [type=jsonparse path="data"]; diff --git a/core/scripts/go.mod b/core/scripts/go.mod index f9537449089..69ddfb58278 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -21,7 +21,7 @@ require ( github.com/prometheus/client_golang v1.17.0 github.com/shopspring/decimal v1.3.1 github.com/smartcontractkit/chainlink-automation v1.0.3 - github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e + github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034 github.com/smartcontractkit/chainlink-vrf v0.0.0-20240222010609-cd67d123c772 github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000 github.com/smartcontractkit/libocr v0.0.0-20240419185742-fd3cab206b2c @@ -259,8 +259,8 @@ require ( github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540 // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917 // indirect - github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20 // indirect - github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0 // indirect + github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b // indirect + github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696 // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 // indirect github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230906073235-9e478e5e19f1 // indirect github.com/smartcontractkit/wsrpc v0.8.1 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 8788867bc8f..9745a09531e 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1185,18 +1185,18 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.3 h1:h/ijT0NiyV06VxYVgcNfsE3+8OEzT3Q0Z9au0z1BPWs= github.com/smartcontractkit/chainlink-automation v1.0.3/go.mod h1:RjboV0Qd7YP+To+OrzHGXaxUxoSONveCoAK2TQ1INLU= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e h1:+lMjCyABWYAEr0ueTKheYHe9YbUx27UP+zpUOi+7Jz4= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e/go.mod h1:DUZccDEW98n+J1mhdWGO7wr/Njad9p9Fzks839JN7Rs= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034 h1:y2AKnwEybyhr7LEvBRn0RoLBABuckvB6S9gQJMEDrgU= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034/go.mod h1:DUZccDEW98n+J1mhdWGO7wr/Njad9p9Fzks839JN7Rs= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d h1:5tgMC5Gi2UAOKZ+m28W8ubjLeR0pQCAcrz6eQ0rW510= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d/go.mod h1:0UNuO3nDt9MFsZPaHJBEUolxVkN0iC69j1ccDp95e8k= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540 h1:xFSv8561jsLtF6gYZr/zW2z5qUUAkcFkApin2mnbYTo= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540/go.mod h1:sjAmX8K2kbQhvDarZE1ZZgDgmHJ50s0BBc/66vKY2ek= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917 h1:MD80ZRCTvxxJ8PBmhtrKoTnky8cVNYrCrIBLVRbrOM0= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917/go.mod h1:jwVxhctE6BgLOSSsVq9wbREpZ8Ev34H+UBxeUhESZRs= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20 h1:ybdconEoRBHLwtDKlZKYaeanQ8UoVqdDiaTlPV+qEiI= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20/go.mod h1:QqcZSwLgEIn7YraAIRmomnBMAuVFephiHrIWVlkWbFI= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0 h1:5QNKQTQpIp+0ogXUu9B85tEie473RZttOhYxM2vHyOc= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0/go.mod h1:OiWUTrrpSLLTMh7FINWjEh6mmDJCVPaC4yEsDCVaWdU= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b h1:pJQ3K0kUdOUICIOZBLAN+MaEfloG2t6Nc1+ve057pYc= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b/go.mod h1:QqcZSwLgEIn7YraAIRmomnBMAuVFephiHrIWVlkWbFI= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696 h1:h1E87+z+JcUEfvbJVF56SnZA/YUFE5ewUE61MaR/Ewg= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696/go.mod h1:OiWUTrrpSLLTMh7FINWjEh6mmDJCVPaC4yEsDCVaWdU= github.com/smartcontractkit/chainlink-vrf v0.0.0-20240222010609-cd67d123c772 h1:LQmRsrzzaYYN3wEU1l5tWiccznhvbyGnu2N+wHSXZAo= github.com/smartcontractkit/chainlink-vrf v0.0.0-20240222010609-cd67d123c772/go.mod h1:Kn1Hape05UzFZ7bOUnm3GVsHzP0TNrVmpfXYNHdqGGs= github.com/smartcontractkit/go-plugin v0.0.0-20240208201424-b3b91517de16 h1:TFe+FvzxClblt6qRfqEhUfa4kFQx5UobuoFGO2W4mMo= diff --git a/core/scripts/ocr2vrf/util.go b/core/scripts/ocr2vrf/util.go index e57f349f1fd..d19e7289ab4 100644 --- a/core/scripts/ocr2vrf/util.go +++ b/core/scripts/ocr2vrf/util.go @@ -55,7 +55,7 @@ var ( Enabled = true [[EVM]] - FinalityDepth = 1 + FinalityDepth = 10 ChainID = '%d' [EVM.Transactions] diff --git a/core/scripts/setup_testdb.sh b/core/scripts/setup_testdb.sh index 8f30e159d1c..534c980feb9 100755 --- a/core/scripts/setup_testdb.sh +++ b/core/scripts/setup_testdb.sh @@ -1,5 +1,9 @@ #/bin/sh +function exit_error { + echo "Error: $1" + exit 1 +} # Create a new user and database for development # This script is intended to be run on a local development machine tdir=$(mktemp -d -t db-dev-user) @@ -9,6 +13,7 @@ password="insecurepassword" database="chainlink_development_test" # here document for the SQL commands cat << EOF > $tdir/db-dev-user.sql +DROP DATABASE IF EXISTS $database; -- create a new user and database for development if they don't exist DO \$\$ BEGIN @@ -22,6 +27,7 @@ WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$database')\gexec -- Grant all privileges on the database to the user ALTER DATABASE $database OWNER TO $username; GRANT ALL PRIVILEGES ON DATABASE "$database" TO "$username"; +ALTER USER $username CREATEDB; -- Create a pristine database for testing SELECT 'CREATE DATABASE chainlink_test_pristine WITH OWNER $username;' @@ -38,18 +44,20 @@ echo "########################################################################## echo "##########################################################################################################" echo "" # Run the SQL commands -psql -U postgres -h localhost -f $tdir/db-dev-user.sql +psql -U postgres -h localhost -f $tdir/db-dev-user.sql || exit_error "Failed to create user $username and database $database" #test the connection -PGPASSWORD=$password psql -U $username -h localhost -d $database -c "SELECT 1" && echo "Connection successful" || echo "Connection failed" +PGPASSWORD=$password psql -U $username -h localhost -d $database -c "SELECT 1" || exit_error "Connection failed for $username to $database" -db_url=$(echo "CL_DATABASE_URL=postgresql://chainlink_dev:insecurepassword@localhost:5432/chainlink_development_test") + + +db_url=$(echo "CL_DATABASE_URL=postgresql://$username:$password@localhost:5432/$database?sslmode=disable") echo $db_url repo=$(git rev-parse --show-toplevel) pushd $repo export $db_url -make testdb || echo "Failed to create test database" +make testdb || exit_error "Failed to create test database" popd # Set the database URL in the .dbenv file diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index f847a032d81..59e22f5b2c9 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -200,21 +200,27 @@ func NewApplication(opts ApplicationOpts) (Application, error) { restrictedHTTPClient := opts.RestrictedHTTPClient unrestrictedHTTPClient := opts.UnrestrictedHTTPClient - if opts.CapabilitiesRegistry == nil { + if opts.CapabilitiesRegistry == nil { // for tests only, in prod Registry is always set at this point opts.CapabilitiesRegistry = capabilities.NewRegistry(globalLogger) } var externalPeerWrapper p2ptypes.PeerWrapper if cfg.Capabilities().Peering().Enabled() { - externalPeer := externalp2p.NewExternalPeerWrapper(keyStore.P2P(), cfg.Capabilities().Peering(), globalLogger) + externalPeer := externalp2p.NewExternalPeerWrapper(keyStore.P2P(), cfg.Capabilities().Peering(), opts.DS, globalLogger) signer := externalPeer externalPeerWrapper = externalPeer srvcs = append(srvcs, externalPeerWrapper) + networkSetup, err := capabilities.NewHardcodedDonNetworkSetup() + if err != nil { + return nil, fmt.Errorf("failed to create hardcoded Don network setup: %w", err) + } + // NOTE: RegistrySyncer will depend on a Relayer when fully implemented dispatcher := remote.NewDispatcher(externalPeerWrapper, signer, opts.CapabilitiesRegistry, globalLogger) - registrySyncer := capabilities.NewRegistrySyncer(externalPeerWrapper, opts.CapabilitiesRegistry, dispatcher, globalLogger) + registrySyncer := capabilities.NewRegistrySyncer(externalPeerWrapper, opts.CapabilitiesRegistry, dispatcher, globalLogger, networkSetup) + srvcs = append(srvcs, dispatcher, registrySyncer) } @@ -395,7 +401,6 @@ func NewApplication(opts ApplicationOpts) (Application, error) { delegates[job.Workflow] = workflows.NewDelegate( globalLogger, opts.CapabilitiesRegistry, - legacyEVMChains, workflowORM, func() *p2ptypes.PeerID { if externalPeerWrapper == nil { diff --git a/core/services/chainlink/cfgtest/cfgtest.go b/core/services/chainlink/cfgtest/cfgtest.go index 1438b702fc1..3bf95452650 100644 --- a/core/services/chainlink/cfgtest/cfgtest.go +++ b/core/services/chainlink/cfgtest/cfgtest.go @@ -10,12 +10,12 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/multierr" - "github.com/smartcontractkit/chainlink/v2/core/utils" + "github.com/smartcontractkit/chainlink-common/pkg/config" ) func AssertFieldsNotNil(t *testing.T, s interface{}) { err := assertValNotNil(t, "", reflect.ValueOf(s)) - _, err = utils.MultiErrorList(err) + _, err = config.MultiErrorList(err) assert.NoError(t, err) } diff --git a/core/services/chainlink/config.go b/core/services/chainlink/config.go index d0d25a5e461..200d4973ed1 100644 --- a/core/services/chainlink/config.go +++ b/core/services/chainlink/config.go @@ -12,12 +12,13 @@ import ( solcfg "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" stkcfg "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/config" + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" + evmcfg "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" "github.com/smartcontractkit/chainlink/v2/core/config/docs" "github.com/smartcontractkit/chainlink/v2/core/config/env" "github.com/smartcontractkit/chainlink/v2/core/config/toml" "github.com/smartcontractkit/chainlink/v2/core/store/models" - "github.com/smartcontractkit/chainlink/v2/core/utils" "github.com/smartcontractkit/chainlink/v2/core/utils/config" ) @@ -57,7 +58,7 @@ func (c *Config) warnings() (err error) { deprecationErr := c.deprecationWarnings() warningErr := c.valueWarnings() err = multierr.Append(deprecationErr, warningErr) - _, list := utils.MultiErrorList(err) + _, list := commonconfig.MultiErrorList(err) return list } @@ -76,23 +77,13 @@ func (c *Config) valueWarnings() (err error) { // deprecationWarnings returns an error if the Config contains deprecated fields. // This is typically used before defaults have been applied, with input from the user. func (c *Config) deprecationWarnings() (err error) { - // ChainType xdai is deprecated and has been renamed to gnosis - for _, evm := range c.EVM { - if evm.ChainType != nil && evm.ChainType.Slug() == "xdai" { - err = multierr.Append(err, config.ErrInvalid{ - Name: "EVM.ChainType", - Value: evm.ChainType.Slug(), - Msg: "deprecated and will be removed in v2.13.0, use 'gnosis' instead", - }) - } - } - return + return nil } // Validate returns an error if the Config is not valid for use, as-is. // This is typically used after defaults have been applied. func (c *Config) Validate() error { - if err := config.Validate(c); err != nil { + if err := commonconfig.Validate(c); err != nil { return fmt.Errorf("invalid configuration: %w", err) } return nil @@ -138,22 +129,22 @@ func (c *Config) SetFrom(f *Config) (err error) { c.Core.SetFrom(&f.Core) if err1 := c.EVM.SetFrom(&f.EVM); err1 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err1, "EVM")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err1, "EVM")) } if err2 := c.Cosmos.SetFrom(&f.Cosmos); err2 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err2, "Cosmos")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err2, "Cosmos")) } if err3 := c.Solana.SetFrom(&f.Solana); err3 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err3, "Solana")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err3, "Solana")) } if err4 := c.Starknet.SetFrom(&f.Starknet); err4 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err4, "Starknet")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err4, "Starknet")) } - _, err = utils.MultiErrorList(err) + _, err = commonconfig.MultiErrorList(err) return err } @@ -164,34 +155,34 @@ type Secrets struct { func (s *Secrets) SetFrom(f *Secrets) (err error) { if err2 := s.Database.SetFrom(&f.Database); err2 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err2, "Database")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err2, "Database")) } if err2 := s.Password.SetFrom(&f.Password); err2 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err2, "Password")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err2, "Password")) } if err2 := s.WebServer.SetFrom(&f.WebServer); err2 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err2, "WebServer")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err2, "WebServer")) } if err2 := s.Pyroscope.SetFrom(&f.Pyroscope); err2 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err2, "Pyroscope")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err2, "Pyroscope")) } if err2 := s.Prometheus.SetFrom(&f.Prometheus); err2 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err2, "Prometheus")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err2, "Prometheus")) } if err2 := s.Mercury.SetFrom(&f.Mercury); err2 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err2, "Mercury")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err2, "Mercury")) } if err2 := s.Threshold.SetFrom(&f.Threshold); err2 != nil { - err = multierr.Append(err, config.NamedMultiErrorList(err2, "Threshold")) + err = multierr.Append(err, commonconfig.NamedMultiErrorList(err2, "Threshold")) } - _, err = utils.MultiErrorList(err) + _, err = commonconfig.MultiErrorList(err) return err } @@ -215,7 +206,7 @@ var ErrInvalidSecrets = errors.New("invalid secrets") // Validate validates every consitutent secret and return an accumulated error func (s *Secrets) Validate() error { - if err := config.Validate(s); err != nil { + if err := commonconfig.Validate(s); err != nil { return fmt.Errorf("%w: %s", ErrInvalidSecrets, err) } return nil @@ -237,7 +228,7 @@ func (s *Secrets) ValidateDB() error { } s.setDefaults() v := &dbValidationType{s.Database} - if err := config.Validate(v); err != nil { + if err := commonconfig.Validate(v); err != nil { return fmt.Errorf("%w: %s", ErrInvalidSecrets, err) } return nil diff --git a/core/services/chainlink/config_general.go b/core/services/chainlink/config_general.go index ce34cc47e47..5b4a6271d52 100644 --- a/core/services/chainlink/config_general.go +++ b/core/services/chainlink/config_general.go @@ -135,7 +135,7 @@ func (o GeneralConfigOpts) New() (GeneralConfig, error) { return nil, err } - _, warning := utils.MultiErrorList(o.Config.warnings()) + _, warning := commonconfig.MultiErrorList(o.Config.warnings()) o.Config.setDefaults() if !o.SkipEnv { @@ -220,7 +220,7 @@ func (g *generalConfig) validate(secretsValidationFn func() error) error { secretsValidationFn(), ) - _, errList := utils.MultiErrorList(err) + _, errList := commonconfig.MultiErrorList(err) return errList } @@ -235,7 +235,7 @@ var emptyStringsEnv string func validateEnv() (err error) { defer func() { if err != nil { - _, err = utils.MultiErrorList(err) + _, err = commonconfig.MultiErrorList(err) err = fmt.Errorf("invalid environment: %w", err) } }() diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index e02435a946b..c2b2e288039 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -25,10 +25,10 @@ import ( solcfg "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" stkcfg "github.com/smartcontractkit/chainlink-starknet/relayer/pkg/chainlink/config" + "github.com/smartcontractkit/chainlink/v2/common/client" commonconfig "github.com/smartcontractkit/chainlink/v2/common/config" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmcfg "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" @@ -215,7 +215,7 @@ func TestConfig_Marshal(t *testing.T) { require.NoError(t, err) return &a } - selectionMode := client.NodeSelectionMode_HighestHead + selectionMode := client.NodeSelectionModeHighestHead global := Config{ Core: toml.Core{ @@ -1210,11 +1210,11 @@ func TestConfig_full(t *testing.T) { for c := range got.EVM { addr, err := types.NewEIP55Address("0x2a3e23c6f242F5345320814aC8a1b4E58707D292") require.NoError(t, err) - if got.EVM[c].ChainWriter.FromAddress == nil { - got.EVM[c].ChainWriter.FromAddress = &addr + if got.EVM[c].Workflow.FromAddress == nil { + got.EVM[c].Workflow.FromAddress = &addr } - if got.EVM[c].ChainWriter.ForwarderAddress == nil { - got.EVM[c].ChainWriter.ForwarderAddress = &addr + if got.EVM[c].Workflow.ForwarderAddress == nil { + got.EVM[c].Workflow.ForwarderAddress = &addr } for n := range got.EVM[c].Nodes { if got.EVM[c].Nodes[n].WSURL == nil { @@ -1647,13 +1647,6 @@ func TestConfig_warnings(t *testing.T) { }, expectedErrors: []string{"Tracing.TLSCertPath: invalid value (/path/to/cert.pem): must be empty when Tracing.Mode is 'unencrypted'"}, }, - { - name: "Value warning - ChainType=xdai is deprecated", - config: Config{ - EVM: evmcfg.EVMConfigs{{Chain: evmcfg.Chain{ChainType: commonconfig.NewChainTypeConfig("xdai")}}}, - }, - expectedErrors: []string{"EVM.ChainType: invalid value (xdai): deprecated and will be removed in v2.13.0, use 'gnosis' instead"}, - }, } for _, tt := range tests { diff --git a/core/services/job/models_test.go b/core/services/job/models_test.go index c177b3b81e1..04bbbdee0f0 100644 --- a/core/services/job/models_test.go +++ b/core/services/job/models_test.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/types" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" "github.com/smartcontractkit/chainlink/v2/core/store/models" ) diff --git a/core/services/keeper/upkeep_executer_test.go b/core/services/keeper/upkeep_executer_test.go index cd02fc27d11..c70a92c725c 100644 --- a/core/services/keeper/upkeep_executer_test.go +++ b/core/services/keeper/upkeep_executer_test.go @@ -275,7 +275,7 @@ func Test_UpkeepExecuter_PerformsUpkeep_Happy(t *testing.T) { head := newHead() executer.OnNewLongestChain(testutils.Context(t), &head) // TODO we want to see an errored run result once this is completed - // https://app.shortcut.com/chainlinklabs/story/25397/remove-failearly-flag-from-eth-call-task + // https://smartcontract-it.atlassian.net/browse/ARCHIVE-22186 cltest.AssertPipelineRunsStays(t, jb.PipelineSpecID, db, 0) }) diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index 350cbc8d593..1c70195dd43 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -26,21 +26,25 @@ import ( ocr2keepers20runner "github.com/smartcontractkit/chainlink-automation/pkg/v2/runner" ocr2keepers21config "github.com/smartcontractkit/chainlink-automation/pkg/v3/config" ocr2keepers21 "github.com/smartcontractkit/chainlink-automation/pkg/v3/plugin" + "github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins/ocr3" + + "github.com/smartcontractkit/chainlink/v2/core/config/env" + + "github.com/smartcontractkit/chainlink-vrf/altbn_128" + dkgpkg "github.com/smartcontractkit/chainlink-vrf/dkg" + "github.com/smartcontractkit/chainlink-vrf/ocr2vrf" + "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins" - "github.com/smartcontractkit/chainlink-common/pkg/loop/reportingplugins/ocr3" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/core" llotypes "github.com/smartcontractkit/chainlink-common/pkg/types/llo" "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" - "github.com/smartcontractkit/chainlink-vrf/altbn_128" - dkgpkg "github.com/smartcontractkit/chainlink-vrf/dkg" - "github.com/smartcontractkit/chainlink-vrf/ocr2vrf" + "github.com/smartcontractkit/chainlink/v2/core/bridges" "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" coreconfig "github.com/smartcontractkit/chainlink/v2/core/config" - "github.com/smartcontractkit/chainlink/v2/core/config/env" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" @@ -683,9 +687,9 @@ func (d *Delegate) newServicesGenericPlugin( } oracleArgs.ReportingPluginFactory = plugin srvs = append(srvs, plugin) - oracle, err := libocr2.NewOracle(oracleArgs) - if err != nil { - return nil, err + oracle, oracleErr := libocr2.NewOracle(oracleArgs) + if oracleErr != nil { + return nil, oracleErr } srvs = append(srvs, job.NewServiceAdapter(oracle)) @@ -714,6 +718,28 @@ func (d *Delegate) newServicesGenericPlugin( if ocr3Provider, ok := provider.(types.OCR3ContractTransmitter); ok { contractTransmitter = ocr3Provider.OCR3ContractTransmitter() } + var onchainKeyringAdapter ocr3types.OnchainKeyring[[]byte] + if onchainSigningStrategy.IsMultiChain() { + // We are extracting the config beforehand + keyBundles := map[string]ocr2key.KeyBundle{} + for name := range onchainSigningStrategy.ConfigCopy() { + kbID, ostErr := onchainSigningStrategy.KeyBundleID(name) + if ostErr != nil { + return nil, ostErr + } + os, ostErr := d.ks.Get(kbID) + if ostErr != nil { + return nil, ostErr + } + keyBundles[name] = os + } + onchainKeyringAdapter, err = ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(keyBundles, lggr) + if err != nil { + return nil, err + } + } else { + onchainKeyringAdapter = ocrcommon.NewOCR3OnchainKeyringAdapter(kb) + } oracleArgs := libocr2.OCR3OracleArgs[[]byte]{ BinaryNetworkEndpointFactory: d.peerWrapper.Peer2, V2Bootstrappers: bootstrapPeers, @@ -725,7 +751,7 @@ func (d *Delegate) newServicesGenericPlugin( MonitoringEndpoint: oracleEndpoint, OffchainConfigDigester: provider.OffchainConfigDigester(), OffchainKeyring: kb, - OnchainKeyring: ocrcommon.NewOCR3OnchainKeyringAdapter(kb), + OnchainKeyring: onchainKeyringAdapter, MetricsRegisterer: prometheus.WrapRegistererWith(map[string]string{"job_name": jb.Name.ValueOrZero()}, prometheus.DefaultRegisterer), } oracleArgs.ReportingPluginFactory = plugin diff --git a/core/services/ocr2/plugins/functions/reporting.go b/core/services/ocr2/plugins/functions/reporting.go index d9d68ec9097..f485ecb9d34 100644 --- a/core/services/ocr2/plugins/functions/reporting.go +++ b/core/services/ocr2/plugins/functions/reporting.go @@ -337,7 +337,7 @@ func (r *functionsReporting) Report(ctx context.Context, ts types.ReportTimestam } // TODO: support per-request aggregation method - // https://app.shortcut.com/chainlinklabs/story/57701/per-request-plugin-config + // https://smartcontract-it.atlassian.net/browse/FUN-159 aggregated, errAgg := Aggregate(defaultAggMethod, observations) if errAgg != nil { r.logger.Error("FunctionsReporting Report: error when aggregating reqId", commontypes.LogFields{ diff --git a/core/services/ocr2/plugins/llo/integration_test.go b/core/services/ocr2/plugins/llo/integration_test.go index b6f752541f4..fd955879f56 100644 --- a/core/services/ocr2/plugins/llo/integration_test.go +++ b/core/services/ocr2/plugins/llo/integration_test.go @@ -148,10 +148,7 @@ func TestIntegration_LLO(t *testing.T) { }) } - configDigest := setConfig(t, steve, backend, verifierContract, verifierAddress, nodes, oracles) - channelDefinitions := setChannelDefinitions(t, steve, backend, configStoreContract, streams) - - // Bury everything with finality depth + // Commit blocks to finality depth to ensure LogPoller has finalized blocks to read from ch, err := nodes[0].App.GetRelayers().LegacyEVMChains().Get(testutils.SimulatedChainID.String()) require.NoError(t, err) finalityDepth := ch.Config().EVM().FinalityDepth() @@ -159,9 +156,11 @@ func TestIntegration_LLO(t *testing.T) { backend.Commit() } + configDigest := setConfig(t, steve, backend, verifierContract, verifierAddress, nodes, oracles) + channelDefinitions := setChannelDefinitions(t, steve, backend, configStoreContract, streams) + addBootstrapJob(t, bootstrapNode, chainID, verifierAddress, "job-1") addOCRJobs(t, streams, serverPubKey, serverURL, verifierAddress, bootstrapPeerID, bootstrapNodePort, nodes, configStoreAddress, clientPubKeys, chainID, fromBlock) - t.Run("receives at least one report per feed from each oracle when EAs are at 100% reliability", func(t *testing.T) { // Expect at least one report per channel from each oracle (keyed by transmitter ID) seen := make(map[ocr2types.Account]map[llotypes.ChannelID]struct{}) diff --git a/core/services/ocr2/plugins/mercury/integration_test.go b/core/services/ocr2/plugins/mercury/integration_test.go index 2339117d4e3..680b05bd4d4 100644 --- a/core/services/ocr2/plugins/mercury/integration_test.go +++ b/core/services/ocr2/plugins/mercury/integration_test.go @@ -138,7 +138,6 @@ func integration_MercuryV1(t *testing.T) { detectPanicLogs(t, logObservers) }) lggr := logger.TestLogger(t) - const fromBlock = 1 // cannot use zero, start from block 1 testStartTimeStamp := uint32(time.Now().Unix()) // test vars @@ -185,6 +184,16 @@ func integration_MercuryV1(t *testing.T) { bootstrapNode := Node{App: appBootstrap, KeyBundle: bootstrapKb} logObservers = append(logObservers, observedLogs) + // Commit blocks to finality depth to ensure LogPoller has finalized blocks to read from + ch, err := bootstrapNode.App.GetRelayers().LegacyEVMChains().Get(testutils.SimulatedChainID.String()) + require.NoError(t, err) + finalityDepth := ch.Config().EVM().FinalityDepth() + for i := 0; i < int(finalityDepth); i++ { + backend.Commit() + } + + fromBlock := int(finalityDepth) // cannot use zero, start from finality depth + // Set up n oracles var ( oracles []confighelper.OracleIdentityExtra @@ -216,7 +225,8 @@ func integration_MercuryV1(t *testing.T) { createBridge := func(name string, i int, p *big.Int, borm bridges.ORM) (bridgeName string) { bridge := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - b, err := io.ReadAll(req.Body) + var b []byte + b, err = io.ReadAll(req.Body) require.NoError(t, err) require.Equal(t, `{"data":{"from":"ETH","to":"USD"}}`, string(b)) @@ -225,12 +235,12 @@ func integration_MercuryV1(t *testing.T) { res.WriteHeader(http.StatusOK) val := decimal.NewFromBigInt(p, 0).Div(decimal.NewFromInt(multiplier)).Add(decimal.NewFromInt(int64(i)).Div(decimal.NewFromInt(100))).String() resp := fmt.Sprintf(`{"result": %s}`, val) - _, err := res.Write([]byte(resp)) + _, err = res.Write([]byte(resp)) require.NoError(t, err) } else { res.WriteHeader(http.StatusInternalServerError) resp := `{"error": "pError test error"}` - _, err := res.Write([]byte(resp)) + _, err = res.Write([]byte(resp)) require.NoError(t, err) } })) @@ -272,7 +282,6 @@ func integration_MercuryV1(t *testing.T) { ) } } - // Setup config on contract onchainConfig, err := (datastreamsmercury.StandardOnchainConfigCodec{}).Encode(rawOnchainConfig) require.NoError(t, err) @@ -334,14 +343,6 @@ func integration_MercuryV1(t *testing.T) { backend.Commit() } - // Bury it with finality depth - ch, err := bootstrapNode.App.GetRelayers().LegacyEVMChains().Get(testutils.SimulatedChainID.String()) - require.NoError(t, err) - finalityDepth := ch.Config().EVM().FinalityDepth() - for i := 0; i < int(finalityDepth); i++ { - backend.Commit() - } - t.Run("receives at least one report per feed from each oracle when EAs are at 100% reliability", func(t *testing.T) { // Expect at least one report per feed from each oracle seen := make(map[[32]byte]map[credentials.StaticSizedPublicKey]struct{}) @@ -539,6 +540,14 @@ func integration_MercuryV2(t *testing.T) { bootstrapNode := Node{App: appBootstrap, KeyBundle: bootstrapKb} logObservers = append(logObservers, observedLogs) + // Commit blocks to finality depth to ensure LogPoller has finalized blocks to read from + ch, err := bootstrapNode.App.GetRelayers().LegacyEVMChains().Get(testutils.SimulatedChainID.String()) + require.NoError(t, err) + finalityDepth := ch.Config().EVM().FinalityDepth() + for i := 0; i < int(finalityDepth); i++ { + backend.Commit() + } + // Set up n oracles var ( oracles []confighelper.OracleIdentityExtra @@ -571,7 +580,8 @@ func integration_MercuryV2(t *testing.T) { createBridge := func(name string, i int, p *big.Int, borm bridges.ORM) (bridgeName string) { bridge := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - b, err := io.ReadAll(req.Body) + var b []byte + b, err = io.ReadAll(req.Body) require.NoError(t, err) require.Equal(t, `{"data":{"from":"ETH","to":"USD"}}`, string(b)) @@ -580,12 +590,12 @@ func integration_MercuryV2(t *testing.T) { res.WriteHeader(http.StatusOK) val := decimal.NewFromBigInt(p, 0).Div(decimal.NewFromInt(multiplier)).Add(decimal.NewFromInt(int64(i)).Div(decimal.NewFromInt(100))).String() resp := fmt.Sprintf(`{"result": %s}`, val) - _, err := res.Write([]byte(resp)) + _, err = res.Write([]byte(resp)) require.NoError(t, err) } else { res.WriteHeader(http.StatusInternalServerError) resp := `{"error": "pError test error"}` - _, err := res.Write([]byte(resp)) + _, err = res.Write([]byte(resp)) require.NoError(t, err) } })) @@ -673,14 +683,6 @@ func integration_MercuryV2(t *testing.T) { backend.Commit() } - // Bury it with finality depth - ch, err := bootstrapNode.App.GetRelayers().LegacyEVMChains().Get(testutils.SimulatedChainID.String()) - require.NoError(t, err) - finalityDepth := ch.Config().EVM().FinalityDepth() - for i := 0; i < int(finalityDepth); i++ { - backend.Commit() - } - runTestSetup := func() { // Expect at least one report per feed from each oracle seen := make(map[[32]byte]map[credentials.StaticSizedPublicKey]struct{}) @@ -828,6 +830,14 @@ func integration_MercuryV3(t *testing.T) { bootstrapNode := Node{App: appBootstrap, KeyBundle: bootstrapKb} logObservers = append(logObservers, observedLogs) + // Commit blocks to finality depth to ensure LogPoller has finalized blocks to read from + ch, err := bootstrapNode.App.GetRelayers().LegacyEVMChains().Get(testutils.SimulatedChainID.String()) + require.NoError(t, err) + finalityDepth := ch.Config().EVM().FinalityDepth() + for i := 0; i < int(finalityDepth); i++ { + backend.Commit() + } + // Set up n oracles var ( oracles []confighelper.OracleIdentityExtra @@ -860,7 +870,8 @@ func integration_MercuryV3(t *testing.T) { createBridge := func(name string, i int, p *big.Int, borm bridges.ORM) (bridgeName string) { bridge := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - b, err := io.ReadAll(req.Body) + var b []byte + b, err = io.ReadAll(req.Body) require.NoError(t, err) require.Equal(t, `{"data":{"from":"ETH","to":"USD"}}`, string(b)) @@ -869,12 +880,12 @@ func integration_MercuryV3(t *testing.T) { res.WriteHeader(http.StatusOK) val := decimal.NewFromBigInt(p, 0).Div(decimal.NewFromInt(multiplier)).Add(decimal.NewFromInt(int64(i)).Div(decimal.NewFromInt(100))).String() resp := fmt.Sprintf(`{"result": %s}`, val) - _, err := res.Write([]byte(resp)) + _, err = res.Write([]byte(resp)) require.NoError(t, err) } else { res.WriteHeader(http.StatusInternalServerError) resp := `{"error": "pError test error"}` - _, err := res.Write([]byte(resp)) + _, err = res.Write([]byte(resp)) require.NoError(t, err) } })) @@ -965,14 +976,6 @@ func integration_MercuryV3(t *testing.T) { backend.Commit() } - // Bury it with finality depth - ch, err := bootstrapNode.App.GetRelayers().LegacyEVMChains().Get(testutils.SimulatedChainID.String()) - require.NoError(t, err) - finalityDepth := ch.Config().EVM().FinalityDepth() - for i := 0; i < int(finalityDepth); i++ { - backend.Commit() - } - runTestSetup := func(reqs chan request) { // Expect at least one report per feed from each oracle, per server seen := make(map[[32]byte]map[credentials.StaticSizedPublicKey]struct{}) diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/buffer_v1.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/buffer_v1.go index fbc1da075df..fe15e962e53 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/buffer_v1.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/buffer_v1.go @@ -77,19 +77,26 @@ type logBuffer struct { // map of upkeep id to its queue queues map[string]*upkeepLogQueue lock sync.RWMutex + + // map for then number of times we have enqueued logs for a block number + enqueuedBlocks map[int64]map[string]int + enqueuedBlockLock sync.RWMutex } func NewLogBuffer(lggr logger.Logger, lookback, blockRate, logLimit uint32) LogBuffer { return &logBuffer{ - lggr: lggr.Named("KeepersRegistry.LogEventBufferV1"), - opts: newLogBufferOptions(lookback, blockRate, logLimit), - lastBlockSeen: new(atomic.Int64), - queues: make(map[string]*upkeepLogQueue), + lggr: lggr.Named("KeepersRegistry.LogEventBufferV1"), + opts: newLogBufferOptions(lookback, blockRate, logLimit), + lastBlockSeen: new(atomic.Int64), + enqueuedBlocks: map[int64]map[string]int{}, + queues: make(map[string]*upkeepLogQueue), } } // Enqueue adds logs to the buffer and might also drop logs if the limit for the // given upkeep was exceeded. It will create a new buffer if it does not exist. +// Logs are expected to be enqueued in increasing order of block number. +// All logs for an upkeep on a particular block will be enqueued in a single Enqueue call. // Returns the number of logs that were added and number of logs that were dropped. func (b *logBuffer) Enqueue(uid *big.Int, logs ...logpoller.Log) (int, int) { buf, ok := b.getUpkeepQueue(uid) @@ -97,17 +104,67 @@ func (b *logBuffer) Enqueue(uid *big.Int, logs ...logpoller.Log) (int, int) { buf = newUpkeepLogQueue(b.lggr, uid, b.opts) b.setUpkeepQueue(uid, buf) } - latestBlock := latestBlockNumber(logs...) - if b.lastBlockSeen.Load() < latestBlock { - b.lastBlockSeen.Store(latestBlock) + + latestLogBlock, uniqueBlocks := blockStatistics(logs...) + if lastBlockSeen := b.lastBlockSeen.Load(); lastBlockSeen < latestLogBlock { + b.lastBlockSeen.Store(latestLogBlock) + } else if latestLogBlock < lastBlockSeen { + b.lggr.Debugw("enqueuing logs with a latest block older older than latest seen block", "logBlock", latestLogBlock, "lastBlockSeen", lastBlockSeen) } + + b.trackBlockNumbersForUpkeep(uid, uniqueBlocks) + blockThreshold := b.lastBlockSeen.Load() - int64(b.opts.lookback.Load()) if blockThreshold <= 0 { blockThreshold = 1 } + + b.cleanupEnqueuedBlocks(blockThreshold) + return buf.enqueue(blockThreshold, logs...) } +func (b *logBuffer) cleanupEnqueuedBlocks(blockThreshold int64) { + b.enqueuedBlockLock.Lock() + defer b.enqueuedBlockLock.Unlock() + // clean up enqueued block counts + for block := range b.enqueuedBlocks { + if block < blockThreshold { + delete(b.enqueuedBlocks, block) + } + } +} + +// trackBlockNumbersForUpkeep keeps track of the number of times we enqueue logs for an upkeep, +// for a specific block number. The expectation is that we will only enqueue logs for an upkeep for a +// specific block number once, i.e. all logs for an upkeep for a block, will be enqueued in a single +// enqueue call. In the event that we see upkeep logs enqueued for a particular block more than once, +// we log a message. +func (b *logBuffer) trackBlockNumbersForUpkeep(uid *big.Int, uniqueBlocks map[int64]bool) { + b.enqueuedBlockLock.Lock() + defer b.enqueuedBlockLock.Unlock() + + if uid == nil { + return + } + + for blockNumber := range uniqueBlocks { + if blockNumbers, ok := b.enqueuedBlocks[blockNumber]; ok { + if upkeepBlockInstances, ok := blockNumbers[uid.String()]; ok { + blockNumbers[uid.String()] = upkeepBlockInstances + 1 + b.lggr.Debugw("enqueuing logs again for a previously seen block for this upkeep", "blockNumber", blockNumber, "numberOfEnqueues", b.enqueuedBlocks[blockNumber], "upkeepID", uid.String()) + } else { + blockNumbers[uid.String()] = 1 + } + b.enqueuedBlocks[blockNumber] = blockNumbers + } else { + b.enqueuedBlocks[blockNumber] = map[string]int{ + uid.String(): 1, + } + } + } +} + // Dequeue greedly pulls logs from the buffers. // Returns logs and the number of remaining logs in the buffer. func (b *logBuffer) Dequeue(block int64, blockRate, upkeepLimit, maxResults int, upkeepSelector func(id *big.Int) bool) ([]BufferedLog, int) { diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/buffer_v1_test.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/buffer_v1_test.go index 19f806d35b9..c41dd3d9bcc 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/buffer_v1_test.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/buffer_v1_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" @@ -50,6 +51,96 @@ func TestLogEventBufferV1_SyncFilters(t *testing.T) { require.Equal(t, 1, buf.NumOfUpkeeps()) } +type readableLogger struct { + logger.Logger + DebugwFn func(msg string, keysAndValues ...interface{}) + NamedFn func(name string) logger.Logger + WithFn func(args ...interface{}) logger.Logger +} + +func (l *readableLogger) Debugw(msg string, keysAndValues ...interface{}) { + l.DebugwFn(msg, keysAndValues...) +} + +func (l *readableLogger) Named(name string) logger.Logger { + return l +} + +func (l *readableLogger) With(args ...interface{}) logger.Logger { + return l +} + +func TestLogEventBufferV1_EnqueueViolations(t *testing.T) { + t.Run("enqueuing logs for a block older than latest seen logs a message", func(t *testing.T) { + logReceived := false + readableLogger := &readableLogger{ + DebugwFn: func(msg string, keysAndValues ...interface{}) { + if msg == "enqueuing logs from a block older than latest seen block" { + logReceived = true + assert.Equal(t, "logBlock", keysAndValues[0]) + assert.Equal(t, int64(1), keysAndValues[1]) + assert.Equal(t, "lastBlockSeen", keysAndValues[2]) + assert.Equal(t, int64(2), keysAndValues[3]) + } + }, + } + + logBufferV1 := NewLogBuffer(readableLogger, 10, 20, 1) + + buf := logBufferV1.(*logBuffer) + + buf.Enqueue(big.NewInt(1), + logpoller.Log{BlockNumber: 2, TxHash: common.HexToHash("0x1"), LogIndex: 0}, + logpoller.Log{BlockNumber: 2, TxHash: common.HexToHash("0x1"), LogIndex: 1}, + ) + buf.Enqueue(big.NewInt(2), + logpoller.Log{BlockNumber: 1, TxHash: common.HexToHash("0x2"), LogIndex: 0}, + ) + + assert.Equal(t, 1, buf.enqueuedBlocks[2]["1"]) + assert.Equal(t, 1, buf.enqueuedBlocks[1]["2"]) + assert.True(t, true, logReceived) + }) + + t.Run("enqueuing logs for the same block over multiple calls logs a message", func(t *testing.T) { + logReceived := false + readableLogger := &readableLogger{ + DebugwFn: func(msg string, keysAndValues ...interface{}) { + if msg == "enqueuing logs again for a previously seen block" { + logReceived = true + assert.Equal(t, "blockNumber", keysAndValues[0]) + assert.Equal(t, int64(3), keysAndValues[1]) + assert.Equal(t, "numberOfEnqueues", keysAndValues[2]) + assert.Equal(t, 2, keysAndValues[3]) + } + }, + } + + logBufferV1 := NewLogBuffer(readableLogger, 10, 20, 1) + + buf := logBufferV1.(*logBuffer) + + buf.Enqueue(big.NewInt(1), + logpoller.Log{BlockNumber: 1, TxHash: common.HexToHash("0x1"), LogIndex: 0}, + logpoller.Log{BlockNumber: 1, TxHash: common.HexToHash("0x1"), LogIndex: 1}, + ) + buf.Enqueue(big.NewInt(2), + logpoller.Log{BlockNumber: 2, TxHash: common.HexToHash("0x2"), LogIndex: 0}, + ) + buf.Enqueue(big.NewInt(3), + logpoller.Log{BlockNumber: 3, TxHash: common.HexToHash("0x3a"), LogIndex: 0}, + ) + buf.Enqueue(big.NewInt(3), + logpoller.Log{BlockNumber: 3, TxHash: common.HexToHash("0x3b"), LogIndex: 0}, + ) + + assert.Equal(t, 1, buf.enqueuedBlocks[2]["2"]) + assert.Equal(t, 1, buf.enqueuedBlocks[1]["1"]) + assert.Equal(t, 2, buf.enqueuedBlocks[3]["3"]) + assert.True(t, true, logReceived) + }) +} + func TestLogEventBufferV1_Dequeue(t *testing.T) { tests := []struct { name string @@ -470,3 +561,107 @@ func createDummyLogSequence(n, startIndex int, block int64, tx common.Hash) []lo } return logs } + +func Test_trackBlockNumbersForUpkeep(t *testing.T) { + buf := NewLogBuffer(logger.TestLogger(t), 10, 20, 1) + + logBuffer := buf.(*logBuffer) + + for _, tc := range []struct { + uid *big.Int + uniqueBlocks map[int64]bool + wantEnqueuedBlocks map[int64]map[string]int + }{ + { + uid: big.NewInt(1), + uniqueBlocks: map[int64]bool{ + 1: true, + 2: true, + 3: true, + }, + wantEnqueuedBlocks: map[int64]map[string]int{ + 1: { + "1": 1, + }, + 2: { + "1": 1, + }, + 3: { + "1": 1, + }, + }, + }, + { + uid: big.NewInt(2), + uniqueBlocks: map[int64]bool{ + 1: true, + 2: true, + 3: true, + }, + wantEnqueuedBlocks: map[int64]map[string]int{ + 1: { + "1": 1, + "2": 1, + }, + 2: { + "1": 1, + "2": 1, + }, + 3: { + "1": 1, + "2": 1, + }, + }, + }, + { + uid: big.NewInt(2), + uniqueBlocks: map[int64]bool{ + 3: true, + 4: true, + }, + wantEnqueuedBlocks: map[int64]map[string]int{ + 1: { + "1": 1, + "2": 1, + }, + 2: { + "1": 1, + "2": 1, + }, + 3: { + "1": 1, + "2": 2, + }, + 4: { + "2": 1, + }, + }, + }, + { + uniqueBlocks: map[int64]bool{ + 3: true, + 4: true, + }, + wantEnqueuedBlocks: map[int64]map[string]int{ + 1: { + "1": 1, + "2": 1, + }, + 2: { + "1": 1, + "2": 1, + }, + 3: { + "1": 1, + "2": 2, + }, + 4: { + "2": 1, + }, + }, + }, + } { + logBuffer.trackBlockNumbersForUpkeep(tc.uid, tc.uniqueBlocks) + assert.Equal(t, tc.wantEnqueuedBlocks, logBuffer.enqueuedBlocks) + } +} diff --git a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/log.go b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/log.go index 9156e341688..9603d6da5be 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/log.go +++ b/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/logprovider/log.go @@ -8,7 +8,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" ) -// LogSorter sorts the logs based on block number, tx hash and log index. +// LogSorter sorts the logs primarily by block number, then by log index, and finally by tx hash. // returns true if b should come before a. func LogSorter(a, b logpoller.Log) bool { return LogComparator(a, b) > 0 @@ -57,13 +57,17 @@ func logID(l logpoller.Log) string { return hex.EncodeToString(ext.LogIdentifier()) } -// latestBlockNumber returns the latest block number from the given logs -func latestBlockNumber(logs ...logpoller.Log) int64 { +// blockStatistics returns the latest block number from the given logs, and a map of unique block numbers +func blockStatistics(logs ...logpoller.Log) (int64, map[int64]bool) { var latest int64 + uniqueBlocks := map[int64]bool{} + for _, l := range logs { if l.BlockNumber > latest { latest = l.BlockNumber } + uniqueBlocks[l.BlockNumber] = true } - return latest + + return latest, uniqueBlocks } diff --git a/core/services/ocr2/plugins/ocr2keeper/integration_21_test.go b/core/services/ocr2/plugins/ocr2keeper/integration_21_test.go index 288e7e74fdb..cd992b1ea61 100644 --- a/core/services/ocr2/plugins/ocr2keeper/integration_21_test.go +++ b/core/services/ocr2/plugins/ocr2keeper/integration_21_test.go @@ -655,6 +655,15 @@ func setupNodes(t *testing.T, nodeKeys [5]ethkey.KeyV2, registry *iregistry21.IK bootstrapNode := Node{ appBootstrap, bootstrapTransmitter, bootstrapKb, } + + // Commit blocks to finality depth to ensure LogPoller has finalized blocks to read from + ch, err := bootstrapNode.App.GetRelayers().LegacyEVMChains().Get(testutils.SimulatedChainID.String()) + require.NoError(t, err) + finalityDepth := ch.Config().EVM().FinalityDepth() + for i := 0; i < int(finalityDepth); i++ { + backend.Commit() + } + var ( oracles []confighelper.OracleIdentityExtra nodes []Node diff --git a/core/services/ocr2/validate/validate.go b/core/services/ocr2/validate/validate.go index 9a85d7993a4..00ddfec000d 100644 --- a/core/services/ocr2/validate/validate.go +++ b/core/services/ocr2/validate/validate.go @@ -116,7 +116,7 @@ func validateSpec(ctx context.Context, tree *toml.Tree, spec job.Job, rc plugins case types.OCR2Keeper: return validateOCR2KeeperSpec(spec.OCR2OracleSpec.PluginConfig) case types.Functions: - // TODO validator for DR-OCR spec: https://app.shortcut.com/chainlinklabs/story/54054/ocr-plugin-for-directrequest-ocr + // TODO validator for DR-OCR spec: https://smartcontract-it.atlassian.net/browse/FUN-112 return nil case types.Mercury: return validateOCR2MercurySpec(spec.OCR2OracleSpec.PluginConfig, *spec.OCR2OracleSpec.FeedID) @@ -200,11 +200,31 @@ func (o *OCR2OnchainSigningStrategy) PublicKey() (string, error) { if !ok { return "", nil } - name, ok := pk.(string) + pkString, ok := pk.(string) if !ok { return "", fmt.Errorf("expected string publicKey value, but got: %T", pk) } - return name, nil + return pkString, nil +} + +func (o *OCR2OnchainSigningStrategy) ConfigCopy() job.JSONConfig { + copiedConfig := make(job.JSONConfig) + for k, v := range o.Config { + copiedConfig[k] = v + } + return copiedConfig +} + +func (o *OCR2OnchainSigningStrategy) KeyBundleID(name string) (string, error) { + kbID, ok := o.Config[name] + if !ok { + return "", nil + } + kbIDString, ok := kbID.(string) + if !ok { + return "", fmt.Errorf("expected string %s value, but got: %T", name, kbID) + } + return kbIDString, nil } func validateGenericPluginSpec(ctx context.Context, spec *job.OCR2OracleSpec, rc plugins.RegistrarConfig) error { @@ -222,17 +242,20 @@ func validateGenericPluginSpec(ctx context.Context, spec *job.OCR2OracleSpec, rc return errors.New("generic config invalid: only OCR version 2 and 3 are supported") } - onchainSigningStrategy := OCR2OnchainSigningStrategy{} - err = json.Unmarshal(spec.OnchainSigningStrategy.Bytes(), &onchainSigningStrategy) - if err != nil { - return err - } - pk, err := onchainSigningStrategy.PublicKey() - if err != nil { - return err - } - if pk == "" { - return errors.New("generic config invalid: must provide public key for the onchain signing strategy") + // OnchainSigningStrategy is optional + if spec.OnchainSigningStrategy != nil && len(spec.OnchainSigningStrategy.Bytes()) > 0 { + onchainSigningStrategy := OCR2OnchainSigningStrategy{} + err = json.Unmarshal(spec.OnchainSigningStrategy.Bytes(), &onchainSigningStrategy) + if err != nil { + return err + } + pk, ossErr := onchainSigningStrategy.PublicKey() + if ossErr != nil { + return ossErr + } + if pk == "" { + return errors.New("generic config invalid: must provide public key for the onchain signing strategy") + } } plugEnv := env.NewPlugin(p.PluginName) diff --git a/core/services/ocr2/validate/validate_test.go b/core/services/ocr2/validate/validate_test.go index da896bf4a92..05a10caeaf5 100644 --- a/core/services/ocr2/validate/validate_test.go +++ b/core/services/ocr2/validate/validate_test.go @@ -958,3 +958,36 @@ spec = "a spec" assert.Equal(t, "median", pc.PluginName) assert.Equal(t, "median", pc.TelemetryType) } + +type envelope2 struct { + OnchainSigningStrategy *validate.OCR2OnchainSigningStrategy +} + +func TestOCR2OnchainSigningStrategy_Unmarshal(t *testing.T) { + payload := ` +[onchainSigningStrategy] +strategyName = "single-chain" +[onchainSigningStrategy.config] +evm = "08d14c6eed757414d72055d28de6caf06535806c6a14e450f3a2f1c854420e17" +publicKey = "0x1234567890123456789012345678901234567890" +` + oss := &envelope2{} + tree, err := toml.Load(payload) + require.NoError(t, err) + o := map[string]any{} + err = tree.Unmarshal(&o) + require.NoError(t, err) + b, err := json.Marshal(o) + require.NoError(t, err) + err = json.Unmarshal(b, oss) + require.NoError(t, err) + + pk, err := oss.OnchainSigningStrategy.PublicKey() + require.NoError(t, err) + kbID, err := oss.OnchainSigningStrategy.KeyBundleID("evm") + require.NoError(t, err) + + assert.False(t, oss.OnchainSigningStrategy.IsMultiChain()) + assert.Equal(t, "0x1234567890123456789012345678901234567890", pk) + assert.Equal(t, "08d14c6eed757414d72055d28de6caf06535806c6a14e450f3a2f1c854420e17", kbID) +} diff --git a/core/services/ocrcommon/adapters.go b/core/services/ocrcommon/adapters.go index 1eee437eb6b..372d9e37f15 100644 --- a/core/services/ocrcommon/adapters.go +++ b/core/services/ocrcommon/adapters.go @@ -2,9 +2,16 @@ package ocrcommon import ( "context" + "fmt" + "github.com/pkg/errors" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" ) var _ ocr3types.OnchainKeyring[[]byte] = (*OCR3OnchainKeyringAdapter)(nil) @@ -71,3 +78,87 @@ func (c *OCR3ContractTransmitterAdapter) Transmit(ctx context.Context, digest oc func (c *OCR3ContractTransmitterAdapter) FromAccount() (ocrtypes.Account, error) { return c.ct.FromAccount() } + +var _ ocr3types.OnchainKeyring[[]byte] = (*OCR3OnchainKeyringMultiChainAdapter)(nil) + +type OCR3OnchainKeyringMultiChainAdapter struct { + keyBundles map[string]ocr2key.KeyBundle + publicKey ocrtypes.OnchainPublicKey + lggr logger.Logger +} + +func NewOCR3OnchainKeyringMultiChainAdapter(ost map[string]ocr2key.KeyBundle, lggr logger.Logger) (*OCR3OnchainKeyringMultiChainAdapter, error) { + if len(ost) == 0 { + return nil, errors.New("no key bundles provided") + } + // We don't need to check for the existence of `publicKey` in the keyBundles map because it is required on validation on `validate/validate.go` + return &OCR3OnchainKeyringMultiChainAdapter{ost, ost["publicKey"].PublicKey(), lggr}, nil +} + +func (a *OCR3OnchainKeyringMultiChainAdapter) PublicKey() ocrtypes.OnchainPublicKey { + return a.publicKey +} + +func (a *OCR3OnchainKeyringMultiChainAdapter) getKeyBundleFromInfo(info []byte) (ocr2key.KeyBundle, error) { + unmarshalledInfo := new(structpb.Struct) + err := proto.Unmarshal(info, unmarshalledInfo) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal report info: %v", err) + } + infoMap := unmarshalledInfo.AsMap() + keyBundleName, ok := infoMap["keyBundleName"] + if !ok { + return nil, errors.New("keyBundleName not found in report info") + } + name, ok := keyBundleName.(string) + if !ok { + return nil, errors.New("keyBundleName is not a string") + } + kb, ok := a.keyBundles[name] + if !ok { + return nil, fmt.Errorf("keyBundle not found: %s", name) + } + return kb, nil +} + +func (a *OCR3OnchainKeyringMultiChainAdapter) Sign(digest ocrtypes.ConfigDigest, seqNr uint64, r ocr3types.ReportWithInfo[[]byte]) (signature []byte, err error) { + kb, err := a.getKeyBundleFromInfo(r.Info) + if err != nil { + return nil, fmt.Errorf("sign: failed to get key bundle from report info: %v", err) + } + return kb.Sign(ocrtypes.ReportContext{ + ReportTimestamp: ocrtypes.ReportTimestamp{ + ConfigDigest: digest, + Epoch: uint32(seqNr), + Round: 0, + }, + ExtraHash: [32]byte(make([]byte, 32)), + }, r.Report) +} + +func (a *OCR3OnchainKeyringMultiChainAdapter) Verify(opk ocrtypes.OnchainPublicKey, digest ocrtypes.ConfigDigest, seqNr uint64, ri ocr3types.ReportWithInfo[[]byte], signature []byte) bool { + kb, err := a.getKeyBundleFromInfo(ri.Info) + if err != nil { + a.lggr.Warnf("verify: failed to get key bundle from report info: %v", err) + return false + } + return kb.Verify(opk, ocrtypes.ReportContext{ + ReportTimestamp: ocrtypes.ReportTimestamp{ + ConfigDigest: digest, + Epoch: uint32(seqNr), + Round: 0, + }, + ExtraHash: [32]byte(make([]byte, 32)), + }, ri.Report, signature) +} + +func (a *OCR3OnchainKeyringMultiChainAdapter) MaxSignatureLength() int { + maxLength := -1 + for _, kb := range a.keyBundles { + l := kb.MaxSignatureLength() + if l > maxLength { + maxLength = l + } + } + return maxLength +} diff --git a/core/services/ocrcommon/adapters_test.go b/core/services/ocrcommon/adapters_test.go index 669e015e7bc..fed854b0b32 100644 --- a/core/services/ocrcommon/adapters_test.go +++ b/core/services/ocrcommon/adapters_test.go @@ -2,15 +2,25 @@ package ocrcommon_test import ( "context" + "encoding/json" "fmt" "reflect" "testing" + "github.com/pelletier/go-toml" "github.com/smartcontractkit/libocr/offchainreporting2/types" "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/keystest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" + keystoreMocks "github.com/smartcontractkit/chainlink/v2/core/services/keystore/mocks" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/validate" "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" ) @@ -105,6 +115,65 @@ func TestOCR3OnchainKeyringAdapter(t *testing.T) { require.Equal(t, maxSignatureLength, kr.MaxSignatureLength()) } +type envelope struct { + OnchainSigningStrategy *validate.OCR2OnchainSigningStrategy +} + +func TestNewOCR3OnchainKeyringMultiChainAdapter(t *testing.T) { + payload := ` +[onchainSigningStrategy] +strategyName = "single-chain" +[onchainSigningStrategy.config] +evm = "08d14c6eed757414d72055d28de6caf06535806c6a14e450f3a2f1c854420e17" +publicKey = "pub-key" +` + oss := &envelope{} + tree, err := toml.Load(payload) + require.NoError(t, err) + o := map[string]any{} + err = tree.Unmarshal(&o) + require.NoError(t, err) + b, err := json.Marshal(o) + require.NoError(t, err) + err = json.Unmarshal(b, oss) + require.NoError(t, err) + reportInfo := ocr3types.ReportWithInfo[[]byte]{ + Report: []byte("multi-chain-report"), + } + info, err := structpb.NewStruct(map[string]interface{}{ + "keyBundleName": "evm", + }) + require.NoError(t, err) + infoB, err := proto.Marshal(info) + require.NoError(t, err) + reportInfo.Info = infoB + + ks := keystoreMocks.NewOCR2(t) + fakeKey := ocr2key.MustNewInsecure(keystest.NewRandReaderFromSeed(1), "evm") + pk := fakeKey.PublicKey() + ks.On("Get", "pub-key").Return(fakeKey, nil) + ks.On("Get", "08d14c6eed757414d72055d28de6caf06535806c6a14e450f3a2f1c854420e17").Return(fakeKey, nil) + keyBundles := map[string]ocr2key.KeyBundle{} + for name := range oss.OnchainSigningStrategy.ConfigCopy() { + kbID, ostErr := oss.OnchainSigningStrategy.KeyBundleID(name) + require.NoError(t, ostErr) + os, ostErr := ks.Get(kbID) + require.NoError(t, ostErr) + keyBundles[name] = os + } + + adapter, err := ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(keyBundles, logger.TestLogger(t)) + require.NoError(t, err) + _, err = ocrcommon.NewOCR3OnchainKeyringMultiChainAdapter(map[string]ocr2key.KeyBundle{}, logger.TestLogger(t)) + require.Error(t, err, "no key bundles provided") + + sig, err := adapter.Sign(configDigest, seqNr, reportInfo) + assert.NoError(t, err) + assert.True(t, adapter.Verify(pk, configDigest, seqNr, reportInfo, sig)) + assert.Equal(t, pk, adapter.PublicKey()) + assert.Equal(t, fakeKey.MaxSignatureLength(), adapter.MaxSignatureLength()) +} + var _ ocrtypes.ContractTransmitter = (*fakeContractTransmitter)(nil) type fakeContractTransmitter struct { diff --git a/core/services/ocrcommon/discoverer_database.go b/core/services/ocrcommon/discoverer_database.go index ea75f9e6d21..051105b746d 100644 --- a/core/services/ocrcommon/discoverer_database.go +++ b/core/services/ocrcommon/discoverer_database.go @@ -2,6 +2,7 @@ package ocrcommon import ( "context" + "fmt" "github.com/lib/pq" "github.com/pkg/errors" @@ -14,35 +15,60 @@ import ( var _ ocrnetworking.DiscovererDatabase = &DiscovererDatabase{} +const ( + // ocrDiscovererTable is the name of the table used to store OCR announcements + ocrDiscovererTable = "ocr_discoverer_announcements" + // don2donDiscovererTable is the name of the table used to store DON2DON announcements + don2donDiscovererTable = "don2don_discoverer_announcements" +) + +// DiscovererDatabase is a key-value store for p2p announcements +// that are based on the RageP2P library and bootstrap nodes type DiscovererDatabase struct { - ds sqlutil.DataSource - peerID string + ds sqlutil.DataSource + peerID string + tableName string +} + +// NewOCRDiscovererDatabase creates a new DiscovererDatabase for OCR announcements +func NewOCRDiscovererDatabase(ds sqlutil.DataSource, peerID string) *DiscovererDatabase { + return &DiscovererDatabase{ + ds: ds, + peerID: peerID, + tableName: ocrDiscovererTable, + } } -func NewDiscovererDatabase(ds sqlutil.DataSource, peerID string) *DiscovererDatabase { +// NewDON2DONDiscovererDatabase creates a new DiscovererDatabase for DON2DON announcements +func NewDON2DONDiscovererDatabase(ds sqlutil.DataSource, peerID string) *DiscovererDatabase { return &DiscovererDatabase{ - ds, - peerID, + ds: ds, + peerID: peerID, + tableName: don2donDiscovererTable, } } // StoreAnnouncement has key-value-store semantics and stores a peerID (key) and an associated serialized // announcement (value). func (d *DiscovererDatabase) StoreAnnouncement(ctx context.Context, peerID string, ann []byte) error { - _, err := d.ds.ExecContext(ctx, ` -INSERT INTO ocr_discoverer_announcements (local_peer_id, remote_peer_id, ann, created_at, updated_at) -VALUES ($1,$2,$3,NOW(),NOW()) ON CONFLICT (local_peer_id, remote_peer_id) DO UPDATE SET + q := fmt.Sprintf(` +INSERT INTO %s (local_peer_id, remote_peer_id, ann, created_at, updated_at) +VALUES ($1,$2,$3,NOW(),NOW()) ON CONFLICT (local_peer_id, remote_peer_id) DO UPDATE SET ann = EXCLUDED.ann, updated_at = EXCLUDED.updated_at -;`, d.peerID, peerID, ann) +;`, d.tableName) + + _, err := d.ds.ExecContext(ctx, + q, d.peerID, peerID, ann) return errors.Wrap(err, "DiscovererDatabase failed to StoreAnnouncement") } // ReadAnnouncements returns one serialized announcement (if available) for each of the peerIDs in the form of a map // keyed by each announcement's corresponding peer ID. func (d *DiscovererDatabase) ReadAnnouncements(ctx context.Context, peerIDs []string) (results map[string][]byte, err error) { - rows, err := d.ds.QueryContext(ctx, ` -SELECT remote_peer_id, ann FROM ocr_discoverer_announcements WHERE remote_peer_id = ANY($1) AND local_peer_id = $2`, pq.Array(peerIDs), d.peerID) + q := fmt.Sprintf(`SELECT remote_peer_id, ann FROM %s WHERE remote_peer_id = ANY($1) AND local_peer_id = $2`, d.tableName) + + rows, err := d.ds.QueryContext(ctx, q, pq.Array(peerIDs), d.peerID) if err != nil { return nil, errors.Wrap(err, "DiscovererDatabase failed to ReadAnnouncements") } diff --git a/core/services/ocrcommon/discoverer_database_test.go b/core/services/ocrcommon/discoverer_database_test.go index 30fb02a8265..16c6d26a42d 100644 --- a/core/services/ocrcommon/discoverer_database_test.go +++ b/core/services/ocrcommon/discoverer_database_test.go @@ -3,6 +3,7 @@ package ocrcommon_test import ( "crypto/ed25519" "crypto/rand" + "fmt" "testing" ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" @@ -21,66 +22,87 @@ func Test_DiscovererDatabase(t *testing.T) { localPeerID1 := mustRandomP2PPeerID(t) localPeerID2 := mustRandomP2PPeerID(t) - dd1 := ocrcommon.NewDiscovererDatabase(db, localPeerID1.Raw()) - dd2 := ocrcommon.NewDiscovererDatabase(db, localPeerID2.Raw()) - - ctx := testutils.Context(t) - - t.Run("StoreAnnouncement writes a value", func(t *testing.T) { - ann := []byte{1, 2, 3} - err := dd1.StoreAnnouncement(ctx, "remote1", ann) - assert.NoError(t, err) - - // test upsert - ann = []byte{4, 5, 6} - err = dd1.StoreAnnouncement(ctx, "remote1", ann) - assert.NoError(t, err) - - // write a different value - ann = []byte{7, 8, 9} - err = dd1.StoreAnnouncement(ctx, "remote2", ann) - assert.NoError(t, err) - }) - - t.Run("ReadAnnouncements reads values filtered by given peerIDs", func(t *testing.T) { - announcements, err := dd1.ReadAnnouncements(ctx, []string{"remote1", "remote2"}) - require.NoError(t, err) - - assert.Len(t, announcements, 2) - assert.Equal(t, []byte{4, 5, 6}, announcements["remote1"]) - assert.Equal(t, []byte{7, 8, 9}, announcements["remote2"]) - - announcements, err = dd1.ReadAnnouncements(ctx, []string{"remote1"}) - require.NoError(t, err) - - assert.Len(t, announcements, 1) - assert.Equal(t, []byte{4, 5, 6}, announcements["remote1"]) - }) - - t.Run("is scoped to local peer ID", func(t *testing.T) { - ann := []byte{10, 11, 12} - err := dd2.StoreAnnouncement(ctx, "remote1", ann) - assert.NoError(t, err) - - announcements, err := dd2.ReadAnnouncements(ctx, []string{"remote1"}) - require.NoError(t, err) - assert.Len(t, announcements, 1) - assert.Equal(t, []byte{10, 11, 12}, announcements["remote1"]) - - announcements, err = dd1.ReadAnnouncements(ctx, []string{"remote1"}) - require.NoError(t, err) - assert.Len(t, announcements, 1) - assert.Equal(t, []byte{4, 5, 6}, announcements["remote1"]) - }) - - t.Run("persists data across restarts", func(t *testing.T) { - dd3 := ocrcommon.NewDiscovererDatabase(db, localPeerID1.Raw()) - - announcements, err := dd3.ReadAnnouncements(ctx, []string{"remote1"}) - require.NoError(t, err) - assert.Len(t, announcements, 1) - assert.Equal(t, []byte{4, 5, 6}, announcements["remote1"]) - }) + type test struct { + name string + dd1 *ocrcommon.DiscovererDatabase + dd2 *ocrcommon.DiscovererDatabase + } + + tests := []test{ + { + name: "ocr discoverer database", + dd1: ocrcommon.NewOCRDiscovererDatabase(db, localPeerID1.Raw()), + dd2: ocrcommon.NewOCRDiscovererDatabase(db, localPeerID2.Raw()), + }, + { + name: "don2don discoverer database", + dd1: ocrcommon.NewDON2DONDiscovererDatabase(db, localPeerID1.Raw()), + dd2: ocrcommon.NewDON2DONDiscovererDatabase(db, localPeerID2.Raw()), + }, + } + + for _, tt := range tests { + dd1 := tt.dd1 + dd2 := tt.dd2 + + ctx := testutils.Context(t) + + t.Run(fmt.Sprintf("%s StoreAnnouncement writes a value", tt.name), func(t *testing.T) { + ann := []byte{1, 2, 3} + err := dd1.StoreAnnouncement(ctx, "remote1", ann) + assert.NoError(t, err) + + // test upsert + ann = []byte{4, 5, 6} + err = dd1.StoreAnnouncement(ctx, "remote1", ann) + assert.NoError(t, err) + + // write a different value + ann = []byte{7, 8, 9} + err = dd1.StoreAnnouncement(ctx, "remote2", ann) + assert.NoError(t, err) + }) + + t.Run(fmt.Sprintf("%s ReadAnnouncements reads values filtered by given peerIDs", tt.name), func(t *testing.T) { + announcements, err := dd1.ReadAnnouncements(ctx, []string{"remote1", "remote2"}) + require.NoError(t, err) + + assert.Len(t, announcements, 2) + assert.Equal(t, []byte{4, 5, 6}, announcements["remote1"]) + assert.Equal(t, []byte{7, 8, 9}, announcements["remote2"]) + + announcements, err = dd1.ReadAnnouncements(ctx, []string{"remote1"}) + require.NoError(t, err) + + assert.Len(t, announcements, 1) + assert.Equal(t, []byte{4, 5, 6}, announcements["remote1"]) + }) + + t.Run(fmt.Sprintf("%s is scoped to local peer ID", tt.name), func(t *testing.T) { + ann := []byte{10, 11, 12} + err := dd2.StoreAnnouncement(ctx, "remote1", ann) + assert.NoError(t, err) + + announcements, err := dd2.ReadAnnouncements(ctx, []string{"remote1"}) + require.NoError(t, err) + assert.Len(t, announcements, 1) + assert.Equal(t, []byte{10, 11, 12}, announcements["remote1"]) + + announcements, err = dd1.ReadAnnouncements(ctx, []string{"remote1"}) + require.NoError(t, err) + assert.Len(t, announcements, 1) + assert.Equal(t, []byte{4, 5, 6}, announcements["remote1"]) + }) + + t.Run(fmt.Sprintf("%s persists data across restarts", tt.name), func(t *testing.T) { + dd3 := ocrcommon.NewOCRDiscovererDatabase(db, localPeerID1.Raw()) + + announcements, err := dd3.ReadAnnouncements(ctx, []string{"remote1"}) + require.NoError(t, err) + assert.Len(t, announcements, 1) + assert.Equal(t, []byte{4, 5, 6}, announcements["remote1"]) + }) + } } func mustRandomP2PPeerID(t *testing.T) p2pkey.PeerID { diff --git a/core/services/ocrcommon/peer_wrapper.go b/core/services/ocrcommon/peer_wrapper.go index 97c429f9a5f..762a3d05aa3 100644 --- a/core/services/ocrcommon/peer_wrapper.go +++ b/core/services/ocrcommon/peer_wrapper.go @@ -117,7 +117,7 @@ func (p *SingletonPeerWrapper) peerConfig() (ocrnetworking.PeerConfig, error) { } p.PeerID = key.PeerID() - discovererDB := NewDiscovererDatabase(p.ds, p.PeerID.Raw()) + discovererDB := NewOCRDiscovererDatabase(p.ds, p.PeerID.Raw()) config := p.p2pCfg peerConfig := ocrnetworking.PeerConfig{ diff --git a/core/services/p2p/wrapper/wrapper.go b/core/services/p2p/wrapper/wrapper.go index acb6694b5a3..7b5b92af72e 100644 --- a/core/services/p2p/wrapper/wrapper.go +++ b/core/services/p2p/wrapper/wrapper.go @@ -10,9 +10,12 @@ import ( "github.com/smartcontractkit/libocr/commontypes" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + "github.com/smartcontractkit/chainlink/v2/core/config" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" "github.com/smartcontractkit/chainlink/v2/core/services/p2p" "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" ) @@ -23,16 +26,18 @@ type peerWrapper struct { p2pConfig config.P2P privateKey ed25519.PrivateKey lggr logger.Logger + ds sqlutil.DataSource } var _ types.PeerWrapper = &peerWrapper{} var _ types.Signer = &peerWrapper{} -func NewExternalPeerWrapper(keystoreP2P keystore.P2P, p2pConfig config.P2P, lggr logger.Logger) *peerWrapper { +func NewExternalPeerWrapper(keystoreP2P keystore.P2P, p2pConfig config.P2P, ds sqlutil.DataSource, lggr logger.Logger) *peerWrapper { return &peerWrapper{ keystoreP2P: keystoreP2P, p2pConfig: p2pConfig, lggr: lggr, + ds: ds, } } @@ -42,15 +47,14 @@ func (e *peerWrapper) GetPeer() types.Peer { // convert to "external" P2P PeerConfig, which is independent of OCR // this has to be done in Start() because keystore is not unlocked at construction time -func convertPeerConfig(keystoreP2P keystore.P2P, p2pConfig config.P2P) (p2p.PeerConfig, error) { - key, err := keystoreP2P.GetOrFirst(p2pConfig.PeerID()) +func (e *peerWrapper) convertPeerConfig() (p2p.PeerConfig, error) { + key, err := e.keystoreP2P.GetOrFirst(e.p2pConfig.PeerID()) if err != nil { return p2p.PeerConfig{}, err } - // TODO(KS-106): use real DB - discovererDB := p2p.NewInMemoryDiscovererDatabase() - bootstrappers, err := convertBootstrapperLocators(p2pConfig.V2().DefaultBootstrappers()) + discovererDB := ocrcommon.NewDON2DONDiscovererDatabase(e.ds, key.PeerID().Raw()) + bootstrappers, err := convertBootstrapperLocators(e.p2pConfig.V2().DefaultBootstrappers()) if err != nil { return p2p.PeerConfig{}, err } @@ -58,12 +62,12 @@ func convertPeerConfig(keystoreP2P keystore.P2P, p2pConfig config.P2P) (p2p.Peer peerConfig := p2p.PeerConfig{ PrivateKey: key.PrivKey, - ListenAddresses: p2pConfig.V2().ListenAddresses(), - AnnounceAddresses: p2pConfig.V2().AnnounceAddresses(), + ListenAddresses: e.p2pConfig.V2().ListenAddresses(), + AnnounceAddresses: e.p2pConfig.V2().AnnounceAddresses(), Bootstrappers: bootstrappers, - DeltaReconcile: p2pConfig.V2().DeltaReconcile().Duration(), - DeltaDial: p2pConfig.V2().DeltaDial().Duration(), + DeltaReconcile: e.p2pConfig.V2().DeltaReconcile().Duration(), + DeltaDial: e.p2pConfig.V2().DeltaDial().Duration(), DiscovererDatabase: discovererDB, // NOTE: this is equivalent to prometheus.DefaultRegisterer, but we need to use a separate @@ -95,7 +99,7 @@ func convertBootstrapperLocators(bootstrappers []commontypes.BootstrapperLocator } func (e *peerWrapper) Start(ctx context.Context) error { - cfg, err := convertPeerConfig(e.keystoreP2P, e.p2pConfig) + cfg, err := e.convertPeerConfig() if err != nil { return err } diff --git a/core/services/p2p/wrapper/wrapper_test.go b/core/services/p2p/wrapper/wrapper_test.go index dd91ecaee47..3c164b5c711 100644 --- a/core/services/p2p/wrapper/wrapper_test.go +++ b/core/services/p2p/wrapper/wrapper_test.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" @@ -18,6 +19,8 @@ import ( ) func TestPeerWrapper_CleanStartClose(t *testing.T) { + db := pgtest.NewSqlxDB(t) + lggr := logger.TestLogger(t) port := freeport.GetOne(t) cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { @@ -30,7 +33,7 @@ func TestPeerWrapper_CleanStartClose(t *testing.T) { require.NoError(t, err) keystoreP2P.On("GetOrFirst", mock.Anything).Return(key, nil) - wrapper := wrapper.NewExternalPeerWrapper(keystoreP2P, cfg.Capabilities().Peering(), lggr) + wrapper := wrapper.NewExternalPeerWrapper(keystoreP2P, cfg.Capabilities().Peering(), db, lggr) require.NotNil(t, wrapper) require.NoError(t, wrapper.Start(testutils.Context(t))) require.NoError(t, wrapper.Close()) diff --git a/core/services/pipeline/runner.go b/core/services/pipeline/runner.go index 9ba6c94c535..708a2b07636 100644 --- a/core/services/pipeline/runner.go +++ b/core/services/pipeline/runner.go @@ -232,6 +232,17 @@ func init() { } } +// overtimeContext returns a modified context for overtime work, since tasks are expected to keep running and return +// results, even after context cancellation. +func overtimeContext(ctx context.Context) (context.Context, context.CancelFunc) { + if d, ok := ctx.Deadline(); ok { + // extend deadline + return context.WithDeadline(context.WithoutCancel(ctx), d.Add(overtime)) + } + // remove cancellation + return context.WithoutCancel(ctx), func() {} +} + func (r *runner) ExecuteRun( ctx context.Context, spec Spec, diff --git a/core/services/pipeline/scheduler.go b/core/services/pipeline/scheduler.go index b589c9a7449..0405f3b8b11 100644 --- a/core/services/pipeline/scheduler.go +++ b/core/services/pipeline/scheduler.go @@ -170,7 +170,7 @@ func (s *scheduler) Run() { result := <-s.resultCh // TODO: if for some reason the cleanup didn't succeed and we're stuck waiting for reports forever // we should be able to timeout and finish shutting down - // See: https://app.shortcut.com/chainlinklabs/story/21225/straighten-out-and-clarify-context-usage-in-the-pipeline + // See: https://smartcontract-it.atlassian.net/browse/BCF-994 s.waiting-- diff --git a/core/services/pipeline/task.bridge.go b/core/services/pipeline/task.bridge.go index 7995cf99296..103e5664666 100644 --- a/core/services/pipeline/task.bridge.go +++ b/core/services/pipeline/task.bridge.go @@ -109,7 +109,10 @@ func (t *BridgeTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, inp return Result{Error: errors.Errorf("headers must have an even number of elements")}, runInfo } - url, err := t.getBridgeURLFromName(ctx, name) + overtimeCtx, cancel := overtimeContext(ctx) + defer cancel() + + url, err := t.getBridgeURLFromName(overtimeCtx, name) if err != nil { return Result{Error: err}, runInfo } @@ -181,7 +184,7 @@ func (t *BridgeTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, inp } var cacheErr error - responseBytes, cacheErr = t.orm.GetCachedResponse(ctx, t.dotID, t.specId, cacheDuration) + responseBytes, cacheErr = t.orm.GetCachedResponse(overtimeCtx, t.dotID, t.specId, cacheDuration) if cacheErr != nil { promBridgeCacheErrors.WithLabelValues(t.Name).Inc() if !errors.Is(cacheErr, sql.ErrNoRows) { @@ -217,7 +220,7 @@ func (t *BridgeTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, inp } if !cachedResponse && cacheTTL > 0 { - err := t.orm.UpsertBridgeResponse(ctx, t.dotID, t.specId, responseBytes) + err := t.orm.UpsertBridgeResponse(overtimeCtx, t.dotID, t.specId, responseBytes) if err != nil { lggr.Errorw("Bridge task: failed to upsert response in bridge cache", "err", err) } @@ -241,7 +244,7 @@ func (t *BridgeTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, inp return result, runInfo } -func (t BridgeTask) getBridgeURLFromName(ctx context.Context, name StringParam) (URLParam, error) { +func (t *BridgeTask) getBridgeURLFromName(ctx context.Context, name StringParam) (URLParam, error) { bt, err := t.orm.FindBridge(ctx, bridges.BridgeName(name)) if err != nil { return URLParam{}, errors.Wrapf(err, "could not find bridge with name '%s'", name) diff --git a/core/services/pipeline/task.bridge_test.go b/core/services/pipeline/task.bridge_test.go index 626820a682d..417f781af7a 100644 --- a/core/services/pipeline/task.bridge_test.go +++ b/core/services/pipeline/task.bridge_test.go @@ -1,6 +1,7 @@ package pipeline_test import ( + "context" "encoding/json" "fmt" "io" @@ -1134,3 +1135,74 @@ func TestBridgeTask_AdapterResponseStatusFailure(t *testing.T) { require.False(t, runInfo.IsRetryable) require.False(t, runInfo.IsPending) } + +func TestBridgeTask_AdapterTimeout(t *testing.T) { + t.Parallel() + ctx := testutils.Context(t) + + db := pgtest.NewSqlxDB(t) + cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { + c.WebServer.BridgeCacheTTL = commonconfig.MustNewDuration(1 * time.Minute) + }) + + s1 := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(time.Second) // delay enough to time-out + })) + defer s1.Close() + + 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, + } + c := clhttptest.NewTestLocalOnlyHTTPClient() + trORM := pipeline.NewORM(db, logger.TestLogger(t), cfg.JobPipeline().MaxSuccessfulRuns()) + specID, err := trORM.CreateSpec(ctx, pipeline.Pipeline{}, *models.NewInterval(5 * time.Minute)) + require.NoError(t, err) + task.HelperSetDependencies(cfg.JobPipeline(), cfg.WebServer(), orm, specID, uuid.UUID{}, c) + + // Insert entry 1m in the past, stale value, should not be used in case of EA failure. + _, err = db.ExecContext(ctx, `INSERT INTO bridge_last_value(dot_id, spec_id, value, finished_at) + VALUES($1, $2, $3, $4) ON CONFLICT ON CONSTRAINT bridge_last_value_pkey + DO UPDATE SET value = $3, finished_at = $4;`, task.DotID(), specID, big.NewInt(9700).Bytes(), time.Now()) + require.NoError(t, err) + + vars := pipeline.NewVarsFrom( + map[string]interface{}{ + "jobRun": map[string]interface{}{ + "meta": map[string]interface{}{ + "shouldFail": true, + }, + }, + }, + ) + + t.Run("pre-cancelled", func(t *testing.T) { + ctx, cancel := context.WithCancel(testutils.Context(t)) + cancel() // pre-cancelled + result, runInfo := task.Run(ctx, logger.TestLogger(t), vars, nil) + + require.NoError(t, result.Error) + require.NotNil(t, result.Value) + require.False(t, runInfo.IsRetryable) + require.False(t, runInfo.IsPending) + }) + + t.Run("short", func(t *testing.T) { + ctx, cancel := context.WithTimeout(testutils.Context(t), time.Millisecond) + t.Cleanup(cancel) + result, runInfo := task.Run(ctx, logger.TestLogger(t), vars, nil) + + require.NoError(t, result.Error) + require.NotNil(t, result.Value) + require.False(t, runInfo.IsRetryable) + require.False(t, runInfo.IsPending) + }) +} diff --git a/core/services/relay/evm/cap_encoder.go b/core/services/relay/evm/cap_encoder.go index e0e3a2cf0f5..e9e9c8d54d8 100644 --- a/core/services/relay/evm/cap_encoder.go +++ b/core/services/relay/evm/cap_encoder.go @@ -68,48 +68,57 @@ func (c *capEncoder) Encode(ctx context.Context, input values.Map) ([]byte, erro if err != nil { return nil, err } - // prepend workflowID and workflowExecutionID to the encoded user data - workflowIDbytes, donIDBytes, executionIDBytes, workflowOwnerBytes, err := extractIDs(unwrappedMap) + + metaMap, ok := input.Underlying[consensustypes.MetadataFieldName] + if !ok { + return nil, fmt.Errorf("expected metadata field to be present: %s", consensustypes.MetadataFieldName) + } + + var meta consensustypes.Metadata + err = metaMap.UnwrapTo(&meta) if err != nil { return nil, err } - return append(append(append(append(workflowIDbytes, donIDBytes...), executionIDBytes...), workflowOwnerBytes...), userPayload...), nil + + return prependMetadataFields(meta, userPayload) } -func decodeID(input map[string]any, key string, idLen int) ([]byte, error) { - id, ok := input[key].(string) - if !ok { - return nil, fmt.Errorf("expected %s to be a string", key) +func prependMetadataFields(meta consensustypes.Metadata, userPayload []byte) ([]byte, error) { + // TODO: use all 7 fields from Metadata struct + result := []byte{} + workflowID, err := decodeID(meta.WorkflowID, idLen) + if err != nil { + return nil, err } + result = append(result, workflowID...) - b, err := hex.DecodeString(id) + donID, err := decodeID(meta.DONID, 4) if err != nil { return nil, err } + result = append(result, donID...) - if len(b) != idLen { - return nil, fmt.Errorf("incorrect length for id %s, expected %d bytes, got %d", id, idLen, len(b)) + executionID, err := decodeID(meta.ExecutionID, idLen) + if err != nil { + return nil, err } + result = append(result, executionID...) - return b, nil -} - -// extract workflowID and executionID from the input map, validate and align to 32 bytes -// NOTE: consider requiring them to be exactly 32 bytes to avoid issues with padding -func extractIDs(input map[string]any) ([]byte, []byte, []byte, []byte, error) { - workflowID, err := decodeID(input, consensustypes.WorkflowIDFieldName, idLen) + workflowOwner, err := decodeID(meta.WorkflowOwner, 20) if err != nil { - return nil, nil, nil, nil, err + return nil, err } + result = append(result, workflowOwner...) + return append(result, userPayload...), nil +} - // TODO: source donID and workflowOwner from somewhere - donID := []byte{0, 1, 2, 3} - workflowOwner := make([]byte, 32) - - executionID, err := decodeID(input, consensustypes.ExecutionIDFieldName, idLen) +func decodeID(id string, expectedLen int) ([]byte, error) { + b, err := hex.DecodeString(id) if err != nil { - return nil, nil, nil, nil, err + return nil, err } - - return workflowID, donID, executionID, workflowOwner, nil + if len(b) != expectedLen { + return nil, fmt.Errorf("incorrect length for id %s, expected %d bytes, got %d", id, expectedLen, len(b)) + } + return b, nil } diff --git a/core/services/relay/evm/cap_encoder_test.go b/core/services/relay/evm/cap_encoder_test.go index 8c56fb9075a..7c441491064 100644 --- a/core/services/relay/evm/cap_encoder_test.go +++ b/core/services/relay/evm/cap_encoder_test.go @@ -18,11 +18,10 @@ var ( reportA = []byte{0x01, 0x02, 0x03} reportB = []byte{0xaa, 0xbb, 0xcc, 0xdd} - // hex encoded 32 byte strings workflowID = "15c631d295ef5e32deb99a10ee6804bc4af1385568f9b3363f6552ac6dbb2cef" donID = "00010203" executionID = "8d4e66421db647dd916d3ec28d56188c8d7dae5f808e03d03339ed2562f13bb0" - workflowOwnerID = "0000000000000000000000000000000000000000000000000000000000000000" + workflowOwnerID = "0000000000000000000000000000000000000000" invalidID = "not_valid" wrongLength = "8d4e66" @@ -39,9 +38,8 @@ func TestEVMEncoder_SingleField(t *testing.T) { // output of a DF2.0 aggregator + metadata fields appended by OCR input := map[string]any{ - "Full_reports": []any{reportA, reportB}, - consensustypes.WorkflowIDFieldName: workflowID, - consensustypes.ExecutionIDFieldName: executionID, + "Full_reports": []any{reportA, reportB}, + consensustypes.MetadataFieldName: getMetadata(workflowID), } wrapped, err = values.NewMap(input) require.NoError(t, err) @@ -78,10 +76,9 @@ func TestEVMEncoder_TwoFields(t *testing.T) { // output of a DF2.0 aggregator + metadata fields appended by OCR input := map[string]any{ - "Prices": []any{big.NewInt(234), big.NewInt(456)}, - "Timestamps": []any{int64(111), int64(222)}, - consensustypes.WorkflowIDFieldName: workflowID, - consensustypes.ExecutionIDFieldName: executionID, + "Prices": []any{big.NewInt(234), big.NewInt(456)}, + "Timestamps": []any{int64(111), int64(222)}, + consensustypes.MetadataFieldName: getMetadata(workflowID), } wrapped, err = values.NewMap(input) require.NoError(t, err) @@ -122,8 +119,7 @@ func TestEVMEncoder_Tuple(t *testing.T) { "Prices": []any{big.NewInt(234), big.NewInt(456)}, "Timestamps": []any{int64(111), int64(222)}, }, - consensustypes.WorkflowIDFieldName: workflowID, - consensustypes.ExecutionIDFieldName: executionID, + consensustypes.MetadataFieldName: getMetadata(workflowID), } wrapped, err = values.NewMap(input) require.NoError(t, err) @@ -171,8 +167,7 @@ func TestEVMEncoder_ListOfTuples(t *testing.T) { "Timestamp": int64(222), }, }, - consensustypes.WorkflowIDFieldName: workflowID, - consensustypes.ExecutionIDFieldName: executionID, + consensustypes.MetadataFieldName: getMetadata(workflowID), } wrapped, err = values.NewMap(input) require.NoError(t, err) @@ -208,9 +203,8 @@ func TestEVMEncoder_InvalidIDs(t *testing.T) { // output of a DF2.0 aggregator + metadata fields appended by OCR // using an invalid ID input := map[string]any{ - "Full_reports": []any{reportA, reportB}, - consensustypes.WorkflowIDFieldName: invalidID, - consensustypes.ExecutionIDFieldName: executionID, + "Full_reports": []any{reportA, reportB}, + consensustypes.MetadataFieldName: getMetadata(invalidID), } wrapped, err = values.NewMap(input) require.NoError(t, err) @@ -219,12 +213,20 @@ func TestEVMEncoder_InvalidIDs(t *testing.T) { // using valid hex string of wrong length input = map[string]any{ - "full_reports": []any{reportA, reportB}, - consensustypes.WorkflowIDFieldName: wrongLength, - consensustypes.ExecutionIDFieldName: executionID, + "Full_reports": []any{reportA, reportB}, + consensustypes.MetadataFieldName: getMetadata(wrongLength), } wrapped, err = values.NewMap(input) require.NoError(t, err) _, err = enc.Encode(testutils.Context(t), *wrapped) assert.ErrorContains(t, err, "incorrect length for id") } + +func getMetadata(cid string) consensustypes.Metadata { + return consensustypes.Metadata{ + WorkflowID: cid, + DONID: donID, + ExecutionID: executionID, + WorkflowOwner: workflowOwnerID, + } +} diff --git a/core/services/relay/evm/chain_writer.go b/core/services/relay/evm/chain_writer.go new file mode 100644 index 00000000000..fa62297efce --- /dev/null +++ b/core/services/relay/evm/chain_writer.go @@ -0,0 +1,186 @@ +package evm + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" + + commonservices "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink/v2/common/txmgr" + txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + evmtxmgr "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" + + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +type ChainWriterService interface { + services.ServiceCtx + commontypes.ChainWriter +} + +// Compile-time assertion that chainWriter implements the ChainWriterService interface. +var _ ChainWriterService = (*chainWriter)(nil) + +func NewChainWriterService(logger logger.Logger, client evmclient.Client, txm evmtxmgr.TxManager, config types.ChainWriterConfig) (ChainWriterService, error) { + w := chainWriter{ + logger: logger, + client: client, + txm: txm, + + sendStrategy: txmgr.NewSendEveryStrategy(), + contracts: config.Contracts, + parsedContracts: &parsedTypes{encoderDefs: map[string]types.CodecEntry{}, decoderDefs: map[string]types.CodecEntry{}}, + } + + if config.SendStrategy != nil { + w.sendStrategy = config.SendStrategy + } + + if err := w.parseContracts(); err != nil { + return nil, fmt.Errorf("%w: failed to parse contracts", err) + } + + var err error + if w.encoder, err = w.parsedContracts.toCodec(); err != nil { + return nil, fmt.Errorf("%w: failed to create codec", err) + } + + return &w, nil +} + +type chainWriter struct { + commonservices.StateMachine + + logger logger.Logger + client evmclient.Client + txm evmtxmgr.TxManager + + sendStrategy txmgrtypes.TxStrategy + contracts map[string]*types.ContractConfig + parsedContracts *parsedTypes + + encoder commontypes.Encoder +} + +// SubmitTransaction ... +// +// Note: The codec that ChainWriter uses to encode the parameters for the contract ABI cannot handle +// `nil` values, including for slices. Until the bug is fixed we need to ensure that there are no +// `nil` values passed in the request. +func (w *chainWriter) SubmitTransaction(ctx context.Context, contract, method string, args any, transactionID uuid.UUID, toAddress string, meta *commontypes.TxMeta, value big.Int) error { + if !common.IsHexAddress(toAddress) { + return fmt.Errorf("toAddress is not a valid ethereum address: %v", toAddress) + } + + contractConfig, ok := w.contracts[contract] + if !ok { + return fmt.Errorf("contract config not found: %v", contract) + } + + methodConfig, ok := contractConfig.Configs[method] + if !ok { + return fmt.Errorf("method config not found: %v", method) + } + + calldata, err := w.encoder.Encode(ctx, args, wrapItemType(contract, method, true)) + if err != nil { + return fmt.Errorf("%w: failed to encode args", err) + } + + var checker evmtxmgr.TransmitCheckerSpec + if methodConfig.Checker != "" { + checker.CheckerType = txmgrtypes.TransmitCheckerType(methodConfig.Checker) + } + + req := evmtxmgr.TxRequest{ + FromAddress: methodConfig.FromAddress, + ToAddress: common.HexToAddress(toAddress), + EncodedPayload: calldata, + FeeLimit: methodConfig.GasLimit, + Meta: &txmgrtypes.TxMeta[common.Address, common.Hash]{WorkflowExecutionID: meta.WorkflowExecutionID}, + Strategy: w.sendStrategy, + Checker: checker, + } + + _, err = w.txm.CreateTransaction(ctx, req) + if err != nil { + return fmt.Errorf("%w; failed to create tx", err) + } + + return nil +} + +func (w *chainWriter) parseContracts() error { + for contract, contractConfig := range w.contracts { + abi, err := abi.JSON(strings.NewReader(contractConfig.ContractABI)) + if err != nil { + return fmt.Errorf("%w: failed to parse contract abi", err) + } + + for method, methodConfig := range contractConfig.Configs { + abiMethod, ok := abi.Methods[methodConfig.ChainSpecificName] + if !ok { + return fmt.Errorf("%w: method %s doesn't exist", commontypes.ErrInvalidConfig, methodConfig.ChainSpecificName) + } + + // ABI.Pack prepends the method.ID to the encodings, we'll need the encoder to do the same. + inputMod, err := methodConfig.InputModifications.ToModifier(evmDecoderHooks...) + if err != nil { + return fmt.Errorf("%w: failed to create input mods", err) + } + + input := types.NewCodecEntry(abiMethod.Inputs, abiMethod.ID, inputMod) + + if err = input.Init(); err != nil { + return fmt.Errorf("%w: failed to init codec entry for method %s", err, method) + } + + w.parsedContracts.encoderDefs[wrapItemType(contract, method, true)] = input + } + } + + return nil +} + +func (w *chainWriter) GetTransactionStatus(ctx context.Context, transactionID uuid.UUID) (commontypes.TransactionStatus, error) { + return commontypes.Unknown, fmt.Errorf("not implemented") +} + +func (w *chainWriter) GetFeeComponents(ctx context.Context) (*commontypes.ChainFeeComponents, error) { + return nil, fmt.Errorf("not implemented") +} + +func (w *chainWriter) Close() error { + return w.StopOnce(w.Name(), func() error { + return nil + }) +} + +func (w *chainWriter) HealthReport() map[string]error { + return map[string]error{ + w.Name(): nil, + } +} + +func (w *chainWriter) Name() string { + return "chain-writer" +} + +func (w *chainWriter) Ready() error { + return nil +} + +func (w *chainWriter) Start(ctx context.Context) error { + return w.StartOnce(w.Name(), func() error { + return nil + }) +} diff --git a/core/services/relay/evm/evm.go b/core/services/relay/evm/evm.go index 3f965931596..23740cc8b3b 100644 --- a/core/services/relay/evm/evm.go +++ b/core/services/relay/evm/evm.go @@ -132,7 +132,7 @@ func NewRelayer(lggr logger.Logger, chain legacyevm.Chain, opts RelayerOpts) (*R mercuryORM := mercury.NewORM(opts.DS) lloORM := llo.NewORM(opts.DS, chain.ID()) cdcFactory := llo.NewChannelDefinitionCacheFactory(lggr, lloORM, chain.LogPoller()) - return &Relayer{ + relayer := &Relayer{ ds: opts.DS, chain: chain, lggr: lggr, @@ -143,7 +143,22 @@ func NewRelayer(lggr logger.Logger, chain legacyevm.Chain, opts RelayerOpts) (*R mercuryORM: mercuryORM, transmitterCfg: opts.TransmitterConfig, capabilitiesRegistry: opts.CapabilitiesRegistry, - }, nil + } + + // Initialize write target capability if configuration is defined + if chain.Config().EVM().Workflow().ForwarderAddress() != nil { + ctx := context.Background() + capability, err := NewWriteTarget(ctx, relayer, chain, lggr) + if err != nil { + return nil, fmt.Errorf("failed to initialize write target: %w", err) + } + if err := relayer.capabilitiesRegistry.Add(ctx, capability); err != nil { + return nil, err + } + lggr.Infow("Registered write target", "chain_id", chain.ID()) + } + + return relayer, nil } func (r *Relayer) Name() string { diff --git a/core/services/relay/evm/types/codec_entry.go b/core/services/relay/evm/types/codec_entry.go index 21e5ac59847..a63460b0897 100644 --- a/core/services/relay/evm/types/codec_entry.go +++ b/core/services/relay/evm/types/codec_entry.go @@ -85,7 +85,15 @@ func (entry *codecEntry) EncodingPrefix() []byte { return tmp } -func (entry *codecEntry) Init() error { +func (entry *codecEntry) Init() (err error) { + // Since reflection panics if errors occur, best to recover in case of any unknown errors + defer func() { + if r := recover(); r != nil { + entry.checkedType = nil + entry.nativeType = nil + err = fmt.Errorf("%w: %v", commontypes.ErrInvalidConfig, r) + } + }() if entry.checkedType != nil { return nil } @@ -234,6 +242,7 @@ func createTupleType(curType *abi.Type, converter func(reflect.Type) reflect.Typ checkedFields := make([]reflect.StructField, len(curType.TupleElems)) for i, elm := range curType.TupleElems { name := curType.TupleRawNames[i] + name = strings.ToUpper(name[:1]) + name[1:] nativeFields[i].Name = name checkedFields[i].Name = name nativeArgType, checkedArgType, err := getNativeAndCheckedTypes(elm) diff --git a/core/services/relay/evm/types/codec_entry_test.go b/core/services/relay/evm/types/codec_entry_test.go index 1ea3a9ae576..d4a0dd8edea 100644 --- a/core/services/relay/evm/types/codec_entry_test.go +++ b/core/services/relay/evm/types/codec_entry_test.go @@ -94,6 +94,43 @@ func TestCodecEntry(t *testing.T) { assertHaveSameStructureAndNames(t, iNative.Type(), entry.CheckedType()) }) + t.Run("nested tuple member names are capitalized", func(t *testing.T) { + type1, err := abi.NewType("uint16", "", []abi.ArgumentMarshaling{}) + require.NoError(t, err) + tupleType, err := abi.NewType("tuple", "", []abi.ArgumentMarshaling{ + {Name: "field3", Type: "uint24"}, + {Name: "field4", Type: "int24"}, + }) + require.NoError(t, err) + args := abi.Arguments{ + {Name: "field1", Type: type1}, + {Name: "field2", Type: tupleType}, + } + entry := NewCodecEntry(args, nil, nil) + require.NoError(t, entry.Init()) + + checked := reflect.New(entry.CheckedType()) + iChecked := reflect.Indirect(checked) + f1 := uint16(2) + iChecked.FieldByName("Field1").Set(reflect.ValueOf(&f1)) + f2 := iChecked.FieldByName("Field2") + f2.Set(reflect.New(f2.Type().Elem())) + f2 = reflect.Indirect(f2) + f3 := big.NewInt( /*2^24 - 1*/ 16777215) + setAndVerifyLimit(t, (*uint24)(f3), f3, f2.FieldByName("Field3")) + f4 := big.NewInt( /*2^23 - 1*/ 8388607) + setAndVerifyLimit(t, (*int24)(f4), f4, f2.FieldByName("Field4")) + + native, err := entry.ToNative(checked) + require.NoError(t, err) + iNative := reflect.Indirect(native) + require.Equal(t, iNative.Field(0).Interface(), iChecked.Field(0).Interface()) + nF2 := reflect.Indirect(iNative.Field(1)) + assert.Equal(t, nF2.Field(0).Interface(), f3) + assert.Equal(t, nF2.Field(1).Interface(), f4) + assertHaveSameStructureAndNames(t, iNative.Type(), entry.CheckedType()) + }) + t.Run("unwrapped types", func(t *testing.T) { // This exists to allow you to decode single returned values without naming the parameter wrappedTuple, err := abi.NewType("tuple", "", []abi.ArgumentMarshaling{ diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index b871b820363..6aa0a8c3a82 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -13,6 +13,8 @@ import ( ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" + "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -22,6 +24,26 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/store/models" ) +type ChainWriterConfig struct { + Contracts map[string]*ContractConfig + SendStrategy txmgrtypes.TxStrategy +} + +type ContractConfig struct { + ContractABI string `json:"contractABI" toml:"contractABI"` + // key is genericName from config + Configs map[string]*ChainWriterDefinition `json:"configs" toml:"configs"` +} + +type ChainWriterDefinition struct { + // chain specific contract method name or event type. + ChainSpecificName string `json:"chainSpecificName"` + Checker string `json:"checker"` + FromAddress common.Address `json:"fromAddress"` + GasLimit uint64 `json:"gasLimit"` // TODO(archseer): what if this has to be configured per call? + InputModifications codec.ModifiersConfig `json:"inputModifications,omitempty"` +} + type ChainReaderConfig struct { // Contracts key is contract name Contracts map[string]ChainContractReader `json:"contracts" toml:"contracts"` diff --git a/core/services/relay/evm/write_target.go b/core/services/relay/evm/write_target.go new file mode 100644 index 00000000000..172e4c1423c --- /dev/null +++ b/core/services/relay/evm/write_target.go @@ -0,0 +1,78 @@ +package evm + +import ( + "context" + "encoding/json" + "fmt" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets" + "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" + "github.com/smartcontractkit/chainlink/v2/core/logger" + relayevmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +func NewWriteTarget(ctx context.Context, relayer *Relayer, chain legacyevm.Chain, lggr logger.Logger) (*targets.WriteTarget, error) { + // generate ID based on chain selector + name := fmt.Sprintf("write_%v", chain.ID()) + chainName, err := chainselectors.NameFromChainId(chain.ID().Uint64()) + if err == nil { + name = fmt.Sprintf("write_%v", chainName) + } + + // EVM-specific init + config := chain.Config().EVM().Workflow() + + // Initialize a reader to check whether a value was already transmitted on chain + contractReaderConfigEncoded, err := json.Marshal(relayevmtypes.ChainReaderConfig{ + Contracts: map[string]relayevmtypes.ChainContractReader{ + "forwarder": { + ContractABI: forwarder.KeystoneForwarderABI, + Configs: map[string]*relayevmtypes.ChainReaderDefinition{ + "getTransmitter": { + ChainSpecificName: "getTransmitter", + }, + }, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to marshal contract reader config %v", err) + } + cr, err := relayer.NewContractReader(contractReaderConfigEncoded) + if err != nil { + return nil, err + } + err = cr.Bind(ctx, []commontypes.BoundContract{{ + Address: config.ForwarderAddress().String(), + Name: "forwarder", + }}) + if err != nil { + return nil, err + } + + chainWriterConfig := relayevmtypes.ChainWriterConfig{ + Contracts: map[string]*relayevmtypes.ContractConfig{ + "forwarder": { + ContractABI: forwarder.KeystoneForwarderABI, + Configs: map[string]*relayevmtypes.ChainWriterDefinition{ + "report": { + ChainSpecificName: "report", + Checker: "simulate", + FromAddress: config.FromAddress().Address(), + GasLimit: 200_000, + }, + }, + }, + }, + } + cw, err := NewChainWriterService(lggr.Named("ChainWriter"), chain.Client(), chain.TxManager(), chainWriterConfig) + if err != nil { + return nil, err + } + + return targets.NewWriteTarget(lggr, name, cr, cw, config.ForwarderAddress().String()), nil +} diff --git a/core/services/relay/evm/write_target_test.go b/core/services/relay/evm/write_target_test.go new file mode 100644 index 00000000000..76060dce990 --- /dev/null +++ b/core/services/relay/evm/write_target_test.go @@ -0,0 +1,213 @@ +package evm_test + +import ( + "errors" + "math/big" + "testing" + + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + evmcapabilities "github.com/smartcontractkit/chainlink/v2/core/capabilities" + evmclimocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + txmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm/mocks" + relayevm "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" +) + +var forwardABI = types.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) + +func TestEvmWrite(t *testing.T) { + chain := evmmocks.NewChain(t) + txManager := txmmocks.NewMockEvmTxManager(t) + evmClient := evmclimocks.NewClient(t) + + // This probably isn't the best way to do this, but couldn't find a simpler way to mock the CallContract response + var mockCall []byte + for i := 0; i < 32; i++ { + mockCall = append(mockCall, byte(0)) + } + evmClient.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(mockCall, nil).Maybe() + + chain.On("ID").Return(big.NewInt(11155111)) + chain.On("TxManager").Return(txManager) + chain.On("LogPoller").Return(nil) + chain.On("Client").Return(evmClient) + + cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { + a := testutils.NewAddress() + addr, err2 := types.NewEIP55Address(a.Hex()) + require.NoError(t, err2) + c.EVM[0].Workflow.FromAddress = &addr + + forwarderA := testutils.NewAddress() + forwarderAddr, err2 := types.NewEIP55Address(forwarderA.Hex()) + require.NoError(t, err2) + c.EVM[0].Workflow.ForwarderAddress = &forwarderAddr + }) + evmCfg := evmtest.NewChainScopedConfig(t, cfg) + + chain.On("Config").Return(evmCfg) + + db := pgtest.NewSqlxDB(t) + keyStore := cltest.NewKeyStore(t, db) + + lggr := logger.TestLogger(t) + relayer, err := relayevm.NewRelayer(lggr, chain, relayevm.RelayerOpts{ + DS: db, + CSAETHKeystore: keyStore, + CapabilitiesRegistry: evmcapabilities.NewRegistry(lggr), + }) + require.NoError(t, err) + + txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, nil).Run(func(args mock.Arguments) { + req := args.Get(1).(txmgr.TxRequest) + payload := make(map[string]any) + method := forwardABI.Methods["report"] + err = method.Inputs.UnpackIntoMap(payload, req.EncodedPayload[4:]) + require.NoError(t, err) + require.Equal(t, []byte{0x1, 0x2, 0x3}, payload["rawReport"]) + require.Equal(t, [][]byte{}, payload["signatures"]) + }).Once() + + t.Run("succeeds with valid report", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, lggr) + require.NoError(t, err) + + config, err := values.NewMap(map[string]any{ + "Address": evmCfg.EVM().Workflow().ForwarderAddress().String(), + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": []byte{1, 2, 3}, + "signatures": [][]byte{}, + }, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: inputs, + } + + ch, err := capability.Execute(ctx, req) + require.NoError(t, err) + + response := <-ch + require.Nil(t, response.Err) + }) + + t.Run("succeeds with empty report", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) + require.NoError(t, err) + + config, err := values.NewMap(map[string]any{ + "Address": evmCfg.EVM().Workflow().ForwarderAddress().String(), + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": nil, + }, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: inputs, + } + + ch, err := capability.Execute(ctx, req) + require.NoError(t, err) + + response := <-ch + require.Nil(t, response.Err) + }) + + t.Run("fails with invalid config", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) + require.NoError(t, err) + + invalidConfig, err := values.NewMap(map[string]any{ + "Address": "invalid-address", + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": nil, + }, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: invalidConfig, + Inputs: inputs, + } + + _, err = capability.Execute(ctx, req) + require.Error(t, err) + }) + + t.Run("fails when TXM CreateTransaction returns error", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) + require.NoError(t, err) + + config, err := values.NewMap(map[string]any{ + "Address": evmCfg.EVM().Workflow().ForwarderAddress().String(), + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": []byte{1, 2, 3}, + "signatures": [][]byte{}, + }, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: inputs, + } + + txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, errors.New("TXM error")) + + _, err = capability.Execute(ctx, req) + require.Error(t, err) + }) +} diff --git a/core/services/workflows/delegate.go b/core/services/workflows/delegate.go index 95d2f0ca29d..014b1bdb22a 100644 --- a/core/services/workflows/delegate.go +++ b/core/services/workflows/delegate.go @@ -10,8 +10,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/types/core" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets" - "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" @@ -19,11 +17,10 @@ import ( ) type Delegate struct { - registry core.CapabilitiesRegistry - logger logger.Logger - legacyEVMChains legacyevm.LegacyChainContainer - peerID func() *p2ptypes.PeerID - store store.Store + registry core.CapabilitiesRegistry + logger logger.Logger + peerID func() *p2ptypes.PeerID + store store.Store } var _ job.Delegate = (*Delegate)(nil) @@ -42,25 +39,21 @@ func (d *Delegate) OnDeleteJob(context.Context, job.Job) error { return nil } // ServicesForSpec satisfies the job.Delegate interface. func (d *Delegate) ServicesForSpec(ctx context.Context, spec job.Job) ([]job.ServiceCtx, error) { - // NOTE: we temporarily do registration inside ServicesForSpec, this will be moved out of job specs in the future - err := targets.InitializeWrite(d.registry, d.legacyEVMChains, d.logger) - if err != nil { - d.logger.Errorw("could not initialize writes", err) - } - dinfo, err := initializeDONInfo(d.logger) if err != nil { d.logger.Errorw("could not add initialize don info", err) } cfg := Config{ - Lggr: d.logger, - Spec: spec.WorkflowSpec.Workflow, - WorkflowID: spec.WorkflowSpec.WorkflowID, - Registry: d.registry, - DONInfo: dinfo, - PeerID: d.peerID, - Store: d.store, + Lggr: d.logger, + Spec: spec.WorkflowSpec.Workflow, + WorkflowID: spec.WorkflowSpec.WorkflowID, + WorkflowOwner: spec.WorkflowSpec.WorkflowOwner, + WorkflowName: spec.WorkflowSpec.WorkflowName, + Registry: d.registry, + DONInfo: dinfo, + PeerID: d.peerID, + Store: d.store, } engine, err := NewEngine(cfg) if err != nil { @@ -99,6 +92,7 @@ func initializeDONInfo(lggr logger.Logger) (*capabilities.DON, error) { } return &capabilities.DON{ + ID: "00010203", Members: p2pIDs, Config: capabilities.DONConfig{ SharedSecret: key, @@ -106,8 +100,8 @@ func initializeDONInfo(lggr logger.Logger) (*capabilities.DON, error) { }, nil } -func NewDelegate(logger logger.Logger, registry core.CapabilitiesRegistry, legacyEVMChains legacyevm.LegacyChainContainer, store store.Store, peerID func() *p2ptypes.PeerID) *Delegate { - return &Delegate{logger: logger, registry: registry, legacyEVMChains: legacyEVMChains, store: store, peerID: peerID} +func NewDelegate(logger logger.Logger, registry core.CapabilitiesRegistry, store store.Store, peerID func() *p2ptypes.PeerID) *Delegate { + return &Delegate{logger: logger, registry: registry, store: store, peerID: peerID} } func ValidatedWorkflowSpec(tomlString string) (job.Job, error) { diff --git a/core/services/workflows/engine.go b/core/services/workflows/engine.go index 447339b5e7f..2672ea3fb6f 100644 --- a/core/services/workflows/engine.go +++ b/core/services/workflows/engine.go @@ -15,6 +15,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/core" "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink-common/pkg/workflows" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/transmission" "github.com/smartcontractkit/chainlink/v2/core/logger" p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" "github.com/smartcontractkit/chainlink/v2/core/services/workflows/store" @@ -119,10 +120,10 @@ func (e *Engine) resolveWorkflowCapabilities(ctx context.Context) error { err := e.initializeCapability(ctx, s) if err != nil { - return err + return fmt.Errorf("failed to initialize capability for step %s: %w", s.Ref, err) } - return e.initializeExecutionStrategy(s) + return nil }) return capabilityRegistrationErr @@ -139,6 +140,20 @@ func (e *Engine) initializeCapability(ctx context.Context, step *step) error { return fmt.Errorf("failed to get capability with ref %s: %s", step.ID, err) } + // Special treatment for local targets - wrap into a transmission capability + target, isTarget := cp.(capabilities.TargetCapability) + if isTarget { + capInfo, err2 := target.Info(ctx) + if err2 != nil { + return fmt.Errorf("failed to get info of target capability: %w", err2) + } + + // If the DON is nil this is a local target + if capInfo.DON == nil { + cp = transmission.NewLocalTargetCapability(e.logger, *e.donInfo.PeerID(), *e.donInfo.DON, target) + } + } + // We configure actions, consensus and targets here, and // they all satisfy the `CallbackCapability` interface cc, ok := cp.(capabilities.CallbackCapability) @@ -261,59 +276,6 @@ func (e *Engine) resumeInProgressExecutions(ctx context.Context) error { return nil } -// initializeExecutionStrategy for `step`. -// Broadly speaking, we'll use `immediateExecution` for non-target steps -// and `scheduledExecution` for targets. If we don't have the necessary -// config to initialize a scheduledExecution for a target, we'll fallback to -// using `immediateExecution`. -func (e *Engine) initializeExecutionStrategy(s *step) error { - if s.executionStrategy != nil { - return nil - } - - // If donInfo has no peerID, then the peer wrapper hasn't been initialized. - // Let's error and try again next time around. - if e.donInfo.PeerID() == nil { - return fmt.Errorf("failed to initialize execution strategy: peer ID %s has not been initialized", e.donInfo.PeerID()) - } - - ie := immediateExecution{} - if s.CapabilityType != capabilities.CapabilityTypeTarget { - e.logger.Debugf("initializing step %+v with immediate execution strategy: not a target", s) - s.executionStrategy = ie - return nil - } - - dinfo := e.donInfo - if dinfo.DON == nil { - e.logger.Debugf("initializing target step with immediate execution strategy: donInfo %+v", e.donInfo) - s.executionStrategy = ie - return nil - } - - var position *int - for i, w := range dinfo.Members { - if w == *dinfo.PeerID() { - idx := i - position = &idx - } - } - - if position == nil { - e.logger.Debugf("initializing step %+v with immediate execution strategy: position not found in donInfo %+v", s, e.donInfo) - s.executionStrategy = ie - return nil - } - - s.executionStrategy = scheduledExecution{ - DON: e.donInfo.DON, - Position: *position, - PeerID: e.donInfo.PeerID(), - } - e.logger.Debugf("initializing step %+v with scheduled execution strategy", s) - return nil -} - // registerTrigger is used during the initialization phase to bind a trigger to this workflow func (e *Engine) registerTrigger(ctx context.Context, t *triggerCapability) error { triggerInputs, err := values.NewMap( @@ -334,7 +296,10 @@ func (e *Engine) registerTrigger(ctx context.Context, t *triggerCapability) erro triggerRegRequest := capabilities.CapabilityRequest{ Metadata: capabilities.RequestMetadata{ - WorkflowID: e.workflow.id, + WorkflowID: e.workflow.id, + WorkflowDonID: e.donInfo.ID, + WorkflowName: e.workflow.name, + WorkflowOwner: e.workflow.owner, }, Config: tc, Inputs: triggerInputs, @@ -448,7 +413,7 @@ func (e *Engine) startExecution(ctx context.Context, executionID string, event v ec := &store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ workflows.KeywordTrigger: { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: event, }, Status: store.StatusCompleted, @@ -597,7 +562,7 @@ func (e *Engine) workerForStepRequest(ctx context.Context, msg stepRequest) { l.Debugw("executing on a step event") stepState := &store.WorkflowExecutionStep{ - Outputs: &store.StepOutput{}, + Outputs: store.StepOutput{}, ExecutionID: msg.state.ExecutionID, Ref: msg.stepRef, } @@ -651,10 +616,13 @@ func (e *Engine) executeStep(ctx context.Context, l logger.Logger, msg stepReque Metadata: capabilities.RequestMetadata{ WorkflowID: msg.state.WorkflowID, WorkflowExecutionID: msg.state.ExecutionID, + WorkflowOwner: e.workflow.owner, + WorkflowName: e.workflow.name, + WorkflowDonID: e.donInfo.ID, }, } - output, err := step.executionStrategy.Apply(ctx, l, step.capability, tr) + output, err := executeSyncAndUnwrapSingleValue(ctx, step.capability, tr) if err != nil { return inputs, nil, err } @@ -673,7 +641,10 @@ func (e *Engine) deregisterTrigger(ctx context.Context, t *triggerCapability) er } deregRequest := capabilities.CapabilityRequest{ Metadata: capabilities.RequestMetadata{ - WorkflowID: e.workflow.id, + WorkflowID: e.workflow.id, + WorkflowDonID: e.donInfo.ID, + WorkflowName: e.workflow.name, + WorkflowOwner: e.workflow.owner, }, Inputs: triggerInputs, Config: t.config, @@ -744,6 +715,8 @@ func (e *Engine) Close() error { type Config struct { Spec string WorkflowID string + WorkflowOwner string + WorkflowName string Lggr logger.Logger Registry core.CapabilitiesRegistry MaxWorkerLimit int @@ -820,6 +793,8 @@ func NewEngine(cfg Config) (engine *Engine, err error) { } workflow.id = cfg.WorkflowID + workflow.owner = cfg.WorkflowOwner + workflow.name = cfg.WorkflowName // Instantiate semaphore to put a limit on the number of workers newWorkerCh := make(chan struct{}, cfg.MaxWorkerLimit) @@ -852,3 +827,21 @@ func NewEngine(cfg Config) (engine *Engine, err error) { } return engine, nil } + +// ExecuteSyncAndUnwrapSingleValue is a convenience method that executes a capability synchronously and unwraps the +// result if it is a single value otherwise returns the list. +func executeSyncAndUnwrapSingleValue(ctx context.Context, cap capabilities.CallbackCapability, req capabilities.CapabilityRequest) (values.Value, error) { + l, err := capabilities.ExecuteSync(ctx, cap, req) + if err != nil { + return nil, err + } + + // `ExecuteSync` returns a `values.List` even if there was + // just one return value. If that is the case, let's unwrap the + // single value to make it easier to use in -- for example -- variable interpolation. + if len(l.Underlying) > 1 { + return l, nil + } + + return l.Underlying[0], nil +} diff --git a/core/services/workflows/engine_test.go b/core/services/workflows/engine_test.go index 6abd241e66c..a25568953a8 100644 --- a/core/services/workflows/engine_test.go +++ b/core/services/workflows/engine_test.go @@ -81,10 +81,12 @@ func newTestEngine(t *testing.T, reg *coreCap.Registry, spec string, opts ...fun initFailed := make(chan struct{}) executionFinished := make(chan string, 100) cfg := Config{ - Lggr: logger.TestLogger(t), - Registry: reg, - Spec: spec, - DONInfo: nil, + Lggr: logger.TestLogger(t), + Registry: reg, + Spec: spec, + DONInfo: &capabilities.DON{ + ID: "00010203", + }, PeerID: func() *p2ptypes.PeerID { return &peerID }, maxRetries: 1, retryMs: 100, @@ -212,7 +214,6 @@ func TestEngineWithHardcodedWorkflow(t *testing.T) { capabilities.CapabilityTypeTarget, "a write capability targeting ethereum sepolia testnet", "v1.0.0", - nil, ), func(req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { m := req.Inputs.Underlying["report"].(*values.Map) @@ -296,7 +297,6 @@ func mockTrigger(t *testing.T) (capabilities.TriggerCapability, capabilities.Cap capabilities.CapabilityTypeTrigger, "issues a trigger when a mercury report is received.", "v1.0.0", - nil, ), ch: make(chan capabilities.CapabilityResponse, 10), } @@ -320,7 +320,6 @@ func mockNoopTrigger(t *testing.T) capabilities.TriggerCapability { capabilities.CapabilityTypeTrigger, "issues a trigger when a mercury report is received.", "v1.0.0", - nil, ), ch: make(chan capabilities.CapabilityResponse, 10), } @@ -334,7 +333,6 @@ func mockFailingConsensus() *mockCapability { capabilities.CapabilityTypeConsensus, "an ocr3 consensus capability", "v3.0.0", - nil, ), func(req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { return capabilities.CapabilityResponse{}, errors.New("fatal consensus error") @@ -349,7 +347,6 @@ func mockConsensus() *mockCapability { capabilities.CapabilityTypeConsensus, "an ocr3 consensus capability", "v3.0.0", - nil, ), func(req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { obs := req.Inputs.Underlying["observations"] @@ -376,7 +373,6 @@ func mockTarget() *mockCapability { capabilities.CapabilityTypeTarget, "a write capability targeting polygon mumbai testnet", "v1.0.0", - nil, ), func(req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { m := req.Inputs.Underlying["report"].(*values.Map) @@ -472,7 +468,6 @@ func mockAction() (*mockCapability, values.Value) { capabilities.CapabilityTypeAction, "a read chain action", "v1.0.0", - nil, ), func(req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { return capabilities.CapabilityResponse{ @@ -549,7 +544,7 @@ func TestEngine_ResumesPendingExecutions(t *testing.T) { ec := &store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ workflows.KeywordTrigger: { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: resp, }, Status: store.StatusCompleted, @@ -604,7 +599,7 @@ func TestEngine_TimesOutOldExecutions(t *testing.T) { ec := &store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ workflows.KeywordTrigger: { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: resp, }, Status: store.StatusCompleted, diff --git a/core/services/workflows/execution_strategy.go b/core/services/workflows/execution_strategy.go deleted file mode 100644 index f5da8bca4be..00000000000 --- a/core/services/workflows/execution_strategy.go +++ /dev/null @@ -1,171 +0,0 @@ -package workflows - -import ( - "context" - "fmt" - "time" - - "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/values" - "github.com/smartcontractkit/chainlink/v2/core/logger" - p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" - - "github.com/smartcontractkit/libocr/permutation" - - "golang.org/x/crypto/sha3" -) - -type executionStrategy interface { - Apply(ctx context.Context, l logger.Logger, cap capabilities.CallbackCapability, req capabilities.CapabilityRequest) (values.Value, error) -} - -var _ executionStrategy = immediateExecution{} - -type immediateExecution struct{} - -func (i immediateExecution) Apply(ctx context.Context, lggr logger.Logger, cap capabilities.CallbackCapability, req capabilities.CapabilityRequest) (values.Value, error) { - l, err := capabilities.ExecuteSync(ctx, cap, req) - if err != nil { - return nil, err - } - - // `ExecuteSync` returns a `values.List` even if there was - // just one return value. If that is the case, let's unwrap the - // single value to make it easier to use in -- for example -- variable interpolation. - if len(l.Underlying) > 1 { - return l, nil - } - - return l.Underlying[0], nil -} - -var _ executionStrategy = scheduledExecution{} - -type scheduledExecution struct { - DON *capabilities.DON - PeerID *p2ptypes.PeerID - Position int -} - -var ( - // S = [N] - Schedule_AllAtOnce = "allAtOnce" - // S = [1 * N] - Schedule_OneAtATime = "oneAtATime" -) - -// scheduledExecution generates a pseudo-random transmission schedule, -// and delays execution until a node is required to transmit. -func (d scheduledExecution) Apply(ctx context.Context, lggr logger.Logger, cap capabilities.CallbackCapability, req capabilities.CapabilityRequest) (values.Value, error) { - tc, err := d.transmissionConfig(req.Config) - if err != nil { - return nil, err - } - - info, err := cap.Info(ctx) - if err != nil { - return nil, err - } - - switch { - // Case 1: Local DON - case info.DON == nil: - n := len(d.DON.Members) - key := d.key(d.DON.Config.SharedSecret, req.Metadata.WorkflowID, req.Metadata.WorkflowExecutionID) - sched, err := schedule(tc.Schedule, n) - if err != nil { - return nil, err - } - - picked := permutation.Permutation(n, key) - delay := d.delayFor(d.Position, sched, picked, tc.DeltaStage) - if delay == nil { - lggr.Debugw("skipping transmission: node is not included in schedule") - return nil, nil - } - - lggr.Debugf("execution delayed by %+v", *delay) - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-time.After(*delay): - lggr.Debugw("executing delayed execution") - return immediateExecution{}.Apply(ctx, lggr, cap, req) - } - // Case 2: Remote DON - default: - // TODO: fill in the remote DON case once consensus has been reach on what to do. - lggr.Debugw("remote DON transmission not implemented: using immediate execution") - return immediateExecution{}.Apply(ctx, lggr, cap, req) - } -} - -// `key` uses a shared secret, combined with a workflowID and a workflowExecutionID to generate -// a secret that can later be used to pseudo-randomly determine a schedule for a set of nodes in a DON. -// The addition of the workflowExecutionID -- which nodes don't know ahead of time -- additionally guarantees -// that a malicious coalition of nodes can't "game" the schedule. -// IMPORTANT: changing this function should happen carefully to maintain the guarantee that all nodes -// arrive at the same secret. -func (d scheduledExecution) key(sharedSecret [16]byte, workflowID, workflowExecutionID string) [16]byte { - hash := sha3.NewLegacyKeccak256() - hash.Write(sharedSecret[:]) - hash.Write([]byte(workflowID)) - hash.Write([]byte(workflowExecutionID)) - - var key [16]byte - copy(key[:], hash.Sum(nil)) - return key -} - -type transmissionConfig struct { - Schedule string - DeltaStage time.Duration -} - -func (d scheduledExecution) transmissionConfig(config *values.Map) (transmissionConfig, error) { - var tc struct { - DeltaStage string - Schedule string - } - err := config.UnwrapTo(&tc) - if err != nil { - return transmissionConfig{}, err - } - - duration, err := time.ParseDuration(tc.DeltaStage) - if err != nil { - return transmissionConfig{}, fmt.Errorf("failed to parse DeltaStage %s as duration: %w", tc.DeltaStage, err) - } - - return transmissionConfig{ - Schedule: tc.Schedule, - DeltaStage: duration, - }, nil -} - -func (d scheduledExecution) delayFor(position int, schedule []int, permutation []int, deltaStage time.Duration) *time.Duration { - sum := 0 - for i, s := range schedule { - sum += s - if permutation[position] < sum { - result := time.Duration(i) * deltaStage - return &result - } - } - - return nil -} - -func schedule(sched string, N int) ([]int, error) { - switch sched { - case Schedule_AllAtOnce: - return []int{N}, nil - case Schedule_OneAtATime: - sch := []int{} - for i := 0; i < N; i++ { - sch = append(sch, 1) - } - return sch, nil - } - return nil, fmt.Errorf("unknown schedule %s", sched) -} diff --git a/core/services/workflows/models.go b/core/services/workflows/models.go index cfebb352a2b..d55212a30ca 100644 --- a/core/services/workflows/models.go +++ b/core/services/workflows/models.go @@ -17,7 +17,9 @@ import ( // treated differently due to their nature of being the starting // point of a workflow. type workflow struct { - id string + id string + owner string + name string graph.Graph[string, *step] triggers []*triggerCapability @@ -76,9 +78,8 @@ func (w *workflow) dependents(start string) ([]*step, error) { // step wraps a Vertex with additional context for execution that is mutated by the engine type step struct { workflows.Vertex - capability capabilities.CallbackCapability - config *values.Map - executionStrategy executionStrategy + capability capabilities.CallbackCapability + config *values.Map } type triggerCapability struct { diff --git a/core/services/workflows/state.go b/core/services/workflows/state.go index 218022eae36..6fc61af3954 100644 --- a/core/services/workflows/state.go +++ b/core/services/workflows/state.go @@ -29,7 +29,7 @@ func copyState(es store.WorkflowExecution) store.WorkflowExecution { Ref: step.Ref, Status: step.Status, - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Err: step.Outputs.Err, Value: copiedov, }, diff --git a/core/services/workflows/state_test.go b/core/services/workflows/state_test.go index ccd6cd5004d..a9829a97c74 100644 --- a/core/services/workflows/state_test.go +++ b/core/services/workflows/state_test.go @@ -38,7 +38,7 @@ func TestInterpolateKey(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: values.NewString(""), }, }, @@ -68,7 +68,7 @@ func TestInterpolateKey(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: values.NewString(""), }, }, @@ -82,7 +82,7 @@ func TestInterpolateKey(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Err: errors.New("catastrophic error"), }, }, @@ -96,7 +96,7 @@ func TestInterpolateKey(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: val, }, }, @@ -110,7 +110,7 @@ func TestInterpolateKey(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: val, }, }, @@ -124,7 +124,7 @@ func TestInterpolateKey(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: val, }, }, @@ -138,7 +138,7 @@ func TestInterpolateKey(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: val, }, }, @@ -152,7 +152,7 @@ func TestInterpolateKey(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: val, }, }, @@ -166,7 +166,7 @@ func TestInterpolateKey(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: val, }, }, @@ -180,7 +180,7 @@ func TestInterpolateKey(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: val, }, }, @@ -222,7 +222,7 @@ func TestInterpolateInputsFromState(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: values.NewString(""), }, }, @@ -242,7 +242,7 @@ func TestInterpolateInputsFromState(t *testing.T) { state: store.WorkflowExecution{ Steps: map[string]*store.WorkflowExecutionStep{ "evm_median": { - Outputs: &store.StepOutput{ + Outputs: store.StepOutput{ Value: values.NewString(""), }, }, diff --git a/core/services/workflows/store/models.go b/core/services/workflows/store/models.go index 29a1df154de..27604543ede 100644 --- a/core/services/workflows/store/models.go +++ b/core/services/workflows/store/models.go @@ -24,7 +24,7 @@ type WorkflowExecutionStep struct { Status string Inputs *values.Map - Outputs *StepOutput + Outputs StepOutput UpdatedAt *time.Time } diff --git a/core/services/workflows/store/store_db.go b/core/services/workflows/store/store_db.go index 73acece5b18..e9204efd7b1 100644 --- a/core/services/workflows/store/store_db.go +++ b/core/services/workflows/store/store_db.go @@ -147,20 +147,15 @@ func stepToState(step workflowStepRow) (*WorkflowExecutionStep, error) { outputs = values.FromProto(vProto) } - var so *StepOutput - if outputErr != nil || outputs != nil { - so = &StepOutput{ - Err: outputErr, - Value: outputs, - } - } - return &WorkflowExecutionStep{ ExecutionID: step.WorkflowExecutionID, Ref: step.Ref, Status: step.Status, Inputs: inputs, - Outputs: so, + Outputs: StepOutput{ + Err: outputErr, + Value: outputs, + }, }, nil } @@ -182,10 +177,6 @@ func stateToStep(state *WorkflowExecutionStep) (workflowStepRow, error) { Inputs: inpb, } - if state.Outputs == nil { - return wsr, nil - } - if state.Outputs.Value != nil { p := values.Proto(state.Outputs.Value) ob, err := proto.Marshal(p) diff --git a/core/services/workflows/store/store_db_test.go b/core/services/workflows/store/store_db_test.go index e41f4857363..e30eda1bfc6 100644 --- a/core/services/workflows/store/store_db_test.go +++ b/core/services/workflows/store/store_db_test.go @@ -153,7 +153,7 @@ func Test_StoreDB_UpdateStep(t *testing.T) { require.NoError(t, err) stepOne.Inputs = nm - stepOne.Outputs = &StepOutput{Err: errors.New("some error")} + stepOne.Outputs = StepOutput{Err: errors.New("some error")} es, err = store.UpsertStep(tests.Context(t), stepOne) require.NoError(t, err) @@ -161,7 +161,7 @@ func Test_StoreDB_UpdateStep(t *testing.T) { gotStep := es.Steps[stepOne.Ref] assert.Equal(t, stepOne, gotStep) - stepTwo.Outputs = &StepOutput{Value: nm} + stepTwo.Outputs = StepOutput{Value: nm} es, err = store.UpsertStep(tests.Context(t), stepTwo) require.NoError(t, err) diff --git a/core/store/migrate/migrations/0240_don2don_discoverer.sql b/core/store/migrate/migrations/0240_don2don_discoverer.sql new file mode 100644 index 00000000000..c08b8cd7b11 --- /dev/null +++ b/core/store/migrate/migrations/0240_don2don_discoverer.sql @@ -0,0 +1,14 @@ +-- +goose Up +-- this migration is for the don2don_discoverer_announcements table +-- it is essentially the same as ocr_discoverer_announcements but scoped to the don2don use case +-- both cases are based on RageP2P library and bootstrap nodes. for now but we want to keep their addresses separate to avoid accidental cross-communication +CREATE TABLE don2don_discoverer_announcements ( + local_peer_id text NOT NULL, + remote_peer_id text NOT NULL, + ann bytea NOT NULL, + created_at timestamptz not null, + updated_at timestamptz not null, + PRIMARY KEY(local_peer_id, remote_peer_id) +); +-- +goose Down +DROP TABLE don2don_discoverer_announcements; diff --git a/core/utils/config/validate.go b/core/utils/config/validate.go index 5fbae24ad53..5c62a66ba14 100644 --- a/core/utils/config/validate.go +++ b/core/utils/config/validate.go @@ -2,130 +2,12 @@ package config import ( "fmt" - "reflect" - "strconv" - "strings" "github.com/Masterminds/semver/v3" - "go.uber.org/multierr" "github.com/smartcontractkit/chainlink-common/pkg/config" - "github.com/smartcontractkit/chainlink/v2/core/utils" ) -// Validated configurations impose constraints that must be checked. -type Validated interface { - // ValidateConfig returns nil if the config is valid, otherwise an error describing why it is invalid. - // - // For implementations: - // - Use package multierr to accumulate all errors, rather than returning the first encountered. - // - If an anonymous field also implements ValidateConfig(), it must be called explicitly! - ValidateConfig() error -} - -// Validate returns any errors from calling Validated.ValidateConfig on cfg and any nested types that implement Validated. -func Validate(cfg interface{}) (err error) { - _, err = utils.MultiErrorList(validate(reflect.ValueOf(cfg), true)) - return -} - -func validate(v reflect.Value, checkInterface bool) (err error) { - if checkInterface { - i := v.Interface() - if vc, ok := i.(Validated); ok { - err = multierr.Append(err, vc.ValidateConfig()) - } else if v.CanAddr() { - i = v.Addr().Interface() - if vc, ok := i.(Validated); ok { - err = multierr.Append(err, vc.ValidateConfig()) - } - } - } - - t := v.Type() - if t.Kind() == reflect.Ptr { - if v.IsNil() { - return - } - t = t.Elem() - v = v.Elem() - } - switch t.Kind() { - case reflect.Bool, reflect.Chan, reflect.Complex128, reflect.Complex64, reflect.Float32, reflect.Float64, - reflect.Func, reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int8, reflect.Interface, - reflect.Invalid, reflect.Ptr, reflect.String, reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Uint8, reflect.Uintptr, reflect.UnsafePointer: - return - case reflect.Struct: - for i := 0; i < t.NumField(); i++ { - ft := t.Field(i) - if !ft.IsExported() { - continue - } - fv := v.Field(i) - if !fv.CanInterface() { - continue - } - if fv.Kind() == reflect.Ptr && fv.IsNil() { - continue - } - // skip the interface if Anonymous, since the parent struct inherits the methods - if fe := validate(fv, !ft.Anonymous); fe != nil { - if ft.Anonymous { - err = multierr.Append(err, fe) - } else { - err = multierr.Append(err, NamedMultiErrorList(fe, ft.Name)) - } - } - } - return - case reflect.Map: - iter := v.MapRange() - for iter.Next() { - mk := iter.Key() - mv := iter.Value() - if !v.CanInterface() { - continue - } - if mv.Kind() == reflect.Ptr && mv.IsNil() { - continue - } - if me := validate(mv, true); me != nil { - err = multierr.Append(err, NamedMultiErrorList(me, fmt.Sprintf("%s", mk.Interface()))) - } - } - return - case reflect.Slice, reflect.Array: - for i := 0; i < v.Len(); i++ { - iv := v.Index(i) - if !v.CanInterface() { - continue - } - if iv.Kind() == reflect.Ptr && iv.IsNil() { - continue - } - if me := validate(iv, true); me != nil { - err = multierr.Append(err, NamedMultiErrorList(me, strconv.Itoa(i))) - } - } - return - } - - return fmt.Errorf("should be unreachable: switch missing case for kind: %s", t.Kind()) -} - -func NamedMultiErrorList(err error, name string) error { - l, merr := utils.MultiErrorList(err) - if l == 0 { - return nil - } - msg := strings.ReplaceAll(merr.Error(), "\n", "\n\t") - if l == 1 { - return fmt.Errorf("%s.%s", name, msg) - } - return fmt.Errorf("%s: %s", name, msg) -} - type ErrInvalid = config.ErrInvalid // NewErrDuplicate returns an ErrInvalid with a standard duplicate message. diff --git a/core/utils/errors.go b/core/utils/errors.go deleted file mode 100644 index 5ed8bc25201..00000000000 --- a/core/utils/errors.go +++ /dev/null @@ -1,48 +0,0 @@ -package utils - -import ( - "fmt" - "strings" -) - -type multiErrorList []error - -// MultiErrorList returns an error which formats underlying errors as a list, or nil if err is nil. -func MultiErrorList(err error) (int, error) { - if err == nil { - return 0, nil - } - errs := Flatten(err) - return len(errs), multiErrorList(errs) -} - -func (m multiErrorList) Error() string { - l := len(m) - if l == 1 { - return m[0].Error() - } - var sb strings.Builder - fmt.Fprintf(&sb, "%d errors:", l) - for _, e := range m { - fmt.Fprintf(&sb, "\n\t- %v", e) - } - return sb.String() -} - -func (m multiErrorList) Unwrap() []error { - return m -} - -// Flatten calls `Unwrap() []error` on each error and subsequent returned error that implement the method, returning a fully flattend sequence. -// -//nolint:errorlint // error type checks will fail on wrapped errors. Disabled since we are not doing checks on error types. -func Flatten(errs ...error) (flat []error) { - for _, err := range errs { - if me, ok := err.(interface{ Unwrap() []error }); ok { - flat = append(flat, Flatten(me.Unwrap()...)...) - continue - } - flat = append(flat, err) - } - return -} diff --git a/core/utils/errors_test.go b/core/utils/errors_test.go deleted file mode 100644 index f55b593c455..00000000000 --- a/core/utils/errors_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package utils - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "go.uber.org/multierr" -) - -func TestFlatten(t *testing.T) { - e := []error{ - errors.New("0"), - errors.New("1"), - errors.New("2"), - errors.New("3"), - } - - // nested errors - // [[[0, 1], 2], 3] - err0 := errors.Join(nil, e[0]) - err0 = errors.Join(err0, e[1]) - err0 = errors.Join(err0, e[2]) - err0 = errors.Join(err0, e[3]) - - // flat error - err1 := errors.Join(e...) - - // multierr provides a flat error - err2 := multierr.Append(nil, e[0]) - err2 = multierr.Append(err2, e[1]) - err2 = multierr.Append(err2, e[2]) - err2 = multierr.Append(err2, e[3]) - - params := []struct { - name string - err error - out []error - }{ - {"errors.Join nested", err0, e}, - {"errors.Join flat", err1, e}, - {"multierr.Append", err2, e}, - {"nil", nil, []error{nil}}, - {"single", e[0], []error{e[0]}}, - } - - for _, p := range params { - t.Run(p.name, func(t *testing.T) { - assert.Equal(t, p.out, Flatten(p.err)) - }) - } -} diff --git a/core/web/router.go b/core/web/router.go index 9c5cb4b661d..5e90cea237b 100644 --- a/core/web/router.go +++ b/core/web/router.go @@ -415,7 +415,7 @@ func v2Routes(app chainlink.Application, r *gin.RouterGroup) { {"cosmos", NewCosmosNodesController(app)}, } { if chain.path == "evm" { - // TODO still EVM only https://app.shortcut.com/chainlinklabs/story/26276/multi-chain-type-ui-node-chain-configuration + // TODO still EVM only . Archive ticket: story/26276/multi-chain-type-ui-node-chain-configuration nodes.GET("", paginatedRequest(chain.nc.Index)) } nodes.GET(chain.path, paginatedRequest(chain.nc.Index)) diff --git a/crib/values-profiles/values-dev.yaml.example b/crib/values-profiles/values-dev.yaml.example index 99151149182..fecedf60aaf 100644 --- a/crib/values-profiles/values-dev.yaml.example +++ b/crib/values-profiles/values-dev.yaml.example @@ -31,7 +31,7 @@ chainlink: ChainID = "43113" MinContractPayment = '0' AutoCreateKey = true - FinalityDepth = 1 + FinalityDepth = 10 [[EVM.Nodes]] Name = 'avax-fuji-testnet-official' WSURL = 'wss://api.avax-test.network/ext/bc/C/ws' @@ -41,7 +41,7 @@ chainlink: ChainID = "97" MinContractPayment = '0' AutoCreateKey = true - FinalityDepth = 1 + FinalityDepth = 10 [[EVM.Nodes]] Name = 'bsc-testnet-official' HTTPURL = 'https://data-seed-prebsc-1-s.bnbchain.org:8545' diff --git a/docs/CONFIG.md b/docs/CONFIG.md index caa911fc00a..6c94b1d1f62 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -3447,7 +3447,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'zksync' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '5s' @@ -3500,7 +3500,7 @@ CheckInclusionPercentile = 90 TransactionPercentile = 60 [HeadTracker] -HistoryDepth = 5 +HistoryDepth = 50 MaxBufferSize = 3 SamplingInterval = '1s' MaxAllowedFinalityDepth = 10000 @@ -3537,7 +3537,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'zksync' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '5s' @@ -3590,7 +3590,7 @@ CheckInclusionPercentile = 90 TransactionPercentile = 60 [HeadTracker] -HistoryDepth = 5 +HistoryDepth = 50 MaxBufferSize = 3 SamplingInterval = '1s' MaxAllowedFinalityDepth = 10000 @@ -3627,7 +3627,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'zksync' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '5s' @@ -3680,7 +3680,7 @@ CheckInclusionPercentile = 90 TransactionPercentile = 60 [HeadTracker] -HistoryDepth = 5 +HistoryDepth = 50 MaxBufferSize = 3 SamplingInterval = '1s' MaxAllowedFinalityDepth = 10000 @@ -3808,7 +3808,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'metis' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '15s' @@ -3897,7 +3897,7 @@ GasLimit = 5400000 AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '15s' @@ -3987,7 +3987,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'metis' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '15s' @@ -4167,7 +4167,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'wemix' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '3s' @@ -4257,7 +4257,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'wemix' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '3s' @@ -4346,7 +4346,7 @@ GasLimit = 5400000 AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '15s' @@ -4795,7 +4795,7 @@ GasLimit = 3800000 AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '15s' @@ -5156,7 +5156,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'celo' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '5s' @@ -5245,7 +5245,7 @@ GasLimit = 5400000 AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LinkContractAddress = '0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846' LogBackfillBatchSize = 1000 @@ -5335,7 +5335,7 @@ GasLimit = 5400000 AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LinkContractAddress = '0x5947BB275c521040051D82396192181b413227A3' LogBackfillBatchSize = 1000 @@ -5426,7 +5426,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'celo' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '5s' @@ -5783,7 +5783,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'metis' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '15s' @@ -6504,7 +6504,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'scroll' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '3s' @@ -6594,7 +6594,7 @@ AutoCreateKey = true BlockBackfillDepth = 10 BlockBackfillSkip = false ChainType = 'scroll' -FinalityDepth = 1 +FinalityDepth = 10 FinalityTagEnabled = false LogBackfillBatchSize = 1000 LogPollInterval = '3s' @@ -7076,8 +7076,6 @@ ChainType = 'arbitrum' # Example ChainType is automatically detected from chain ID. Set this to force a certain chain type regardless of chain ID. Available types: `arbitrum`, `celo`, `gnosis`, `kroma`, `metis`, `optimismBedrock`, `scroll`, `wemix`, `xlayer`, `zksync` -`xdai` has been deprecated and will be removed in v2.13.0, use `gnosis` instead. - ### FinalityDepth ```toml FinalityDepth = 50 # Default @@ -8002,9 +8000,9 @@ GasLimit = 5400000 # Default ``` GasLimit controls the gas limit for transmit transactions from ocr2automation job. -## EVM.ChainWriter +## EVM.Workflow ```toml -[EVM.ChainWriter] +[EVM.Workflow] FromAddress = '0x2a3e23c6f242F5345320814aC8a1b4E58707D292' # Example ForwarderAddress = '0x2a3e23c6f242F5345320814aC8a1b4E58707D292' # Example ``` diff --git a/flake.lock b/flake.lock index 260506ff892..da3a69cd248 100644 --- a/flake.lock +++ b/flake.lock @@ -26,15 +26,16 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1715073011, - "narHash": "sha256-fwTWvaOgAUrQwaCcGfeRn1D+n0G4ltr+I+FPb05RPeY=", + "lastModified": 1714727549, + "narHash": "sha256-CWXRTxxcgMfQubJugpeg3yVWIfm70MYTtgaKWKgD60U=", "owner": "shazow", "repo": "foundry.nix", - "rev": "5d2761d546b8712e3faaa416bacc6567007d757a", + "rev": "47cf189ec395eda4b3e0623179d1075c8027ca97", "type": "github" }, "original": { "owner": "shazow", + "ref": "monthly", "repo": "foundry.nix", "type": "github" } diff --git a/flake.nix b/flake.nix index 7ae9a5435bd..f65847455b2 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - foundry.url = "github:shazow/foundry.nix"; + foundry.url = "github:shazow/foundry.nix/monthly"; flake-utils.url = "github:numtide/flake-utils"; foundry.inputs.flake-utils.follows = "flake-utils"; }; diff --git a/go.mod b/go.mod index 58cf2d797df..f9f8cc6000e 100644 --- a/go.mod +++ b/go.mod @@ -72,12 +72,12 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/smartcontractkit/chain-selectors v1.0.10 github.com/smartcontractkit/chainlink-automation v1.0.3 - github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e + github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540 github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917 - github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20 - github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0 + github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b + github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696 github.com/smartcontractkit/chainlink-vrf v0.0.0-20231120191722-fef03814f868 github.com/smartcontractkit/libocr v0.0.0-20240419185742-fd3cab206b2c github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 diff --git a/go.sum b/go.sum index 2085e3c3fd6..05c8c9ffe3c 100644 --- a/go.sum +++ b/go.sum @@ -1171,18 +1171,18 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.3 h1:h/ijT0NiyV06VxYVgcNfsE3+8OEzT3Q0Z9au0z1BPWs= github.com/smartcontractkit/chainlink-automation v1.0.3/go.mod h1:RjboV0Qd7YP+To+OrzHGXaxUxoSONveCoAK2TQ1INLU= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e h1:+lMjCyABWYAEr0ueTKheYHe9YbUx27UP+zpUOi+7Jz4= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e/go.mod h1:DUZccDEW98n+J1mhdWGO7wr/Njad9p9Fzks839JN7Rs= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034 h1:y2AKnwEybyhr7LEvBRn0RoLBABuckvB6S9gQJMEDrgU= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034/go.mod h1:DUZccDEW98n+J1mhdWGO7wr/Njad9p9Fzks839JN7Rs= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d h1:5tgMC5Gi2UAOKZ+m28W8ubjLeR0pQCAcrz6eQ0rW510= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d/go.mod h1:0UNuO3nDt9MFsZPaHJBEUolxVkN0iC69j1ccDp95e8k= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540 h1:xFSv8561jsLtF6gYZr/zW2z5qUUAkcFkApin2mnbYTo= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540/go.mod h1:sjAmX8K2kbQhvDarZE1ZZgDgmHJ50s0BBc/66vKY2ek= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917 h1:MD80ZRCTvxxJ8PBmhtrKoTnky8cVNYrCrIBLVRbrOM0= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917/go.mod h1:jwVxhctE6BgLOSSsVq9wbREpZ8Ev34H+UBxeUhESZRs= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20 h1:ybdconEoRBHLwtDKlZKYaeanQ8UoVqdDiaTlPV+qEiI= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20/go.mod h1:QqcZSwLgEIn7YraAIRmomnBMAuVFephiHrIWVlkWbFI= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0 h1:5QNKQTQpIp+0ogXUu9B85tEie473RZttOhYxM2vHyOc= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0/go.mod h1:OiWUTrrpSLLTMh7FINWjEh6mmDJCVPaC4yEsDCVaWdU= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b h1:pJQ3K0kUdOUICIOZBLAN+MaEfloG2t6Nc1+ve057pYc= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b/go.mod h1:QqcZSwLgEIn7YraAIRmomnBMAuVFephiHrIWVlkWbFI= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696 h1:h1E87+z+JcUEfvbJVF56SnZA/YUFE5ewUE61MaR/Ewg= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696/go.mod h1:OiWUTrrpSLLTMh7FINWjEh6mmDJCVPaC4yEsDCVaWdU= github.com/smartcontractkit/chainlink-vrf v0.0.0-20231120191722-fef03814f868 h1:FFdvEzlYwcuVHkdZ8YnZR/XomeMGbz5E2F2HZI3I3w8= github.com/smartcontractkit/chainlink-vrf v0.0.0-20231120191722-fef03814f868/go.mod h1:Kn1Hape05UzFZ7bOUnm3GVsHzP0TNrVmpfXYNHdqGGs= github.com/smartcontractkit/go-plugin v0.0.0-20240208201424-b3b91517de16 h1:TFe+FvzxClblt6qRfqEhUfa4kFQx5UobuoFGO2W4mMo= diff --git a/integration-tests/docker/test_env/test_env_builder.go b/integration-tests/docker/test_env/test_env_builder.go index f5a5e558572..098b273d9bb 100644 --- a/integration-tests/docker/test_env/test_env_builder.go +++ b/integration-tests/docker/test_env/test_env_builder.go @@ -73,6 +73,7 @@ type CLTestEnvBuilder struct { var DefaultAllowedMessages = []testreporters.AllowedLogMessage{ testreporters.NewAllowedLogMessage("Failed to get LINK balance", "Happens only when we deploy LINK token for test purposes. Harmless.", zapcore.ErrorLevel, testreporters.WarnAboutAllowedMsgs_No), testreporters.NewAllowedLogMessage("Error stopping job service", "It's a known issue with lifecycle. There's ongoing work that will fix it.", zapcore.DPanicLevel, testreporters.WarnAboutAllowedMsgs_No), + testreporters.NewAllowedLogMessage("SLOW SQL QUERY", "Known issue in Automation Node Upgrade Test - https://smartcontract-it.atlassian.net/browse/BCF-3245", zapcore.DPanicLevel, testreporters.WarnAboutAllowedMsgs_No), } var DefaultChainlinkNodeLogScannerSettings = ChainlinkNodeLogScannerSettings{ diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 3d6814244c9..76fb1264b5a 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -27,7 +27,7 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/slack-go/slack v0.12.2 github.com/smartcontractkit/chainlink-automation v1.0.3 - github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e + github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034 github.com/smartcontractkit/chainlink-testing-framework v1.28.17 github.com/smartcontractkit/chainlink-vrf v0.0.0-20231120191722-fef03814f868 github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000 @@ -378,8 +378,8 @@ require ( github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540 // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917 // indirect - github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20 // indirect - github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0 // indirect + github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b // indirect + github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696 // indirect github.com/smartcontractkit/chainlink-testing-framework/grafana v0.0.0-20240328204215-ac91f55f1449 // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 // indirect github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20230906073235-9e478e5e19f1 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 4340728e653..53462f9be60 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1512,18 +1512,18 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.3 h1:h/ijT0NiyV06VxYVgcNfsE3+8OEzT3Q0Z9au0z1BPWs= github.com/smartcontractkit/chainlink-automation v1.0.3/go.mod h1:RjboV0Qd7YP+To+OrzHGXaxUxoSONveCoAK2TQ1INLU= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e h1:+lMjCyABWYAEr0ueTKheYHe9YbUx27UP+zpUOi+7Jz4= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e/go.mod h1:DUZccDEW98n+J1mhdWGO7wr/Njad9p9Fzks839JN7Rs= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034 h1:y2AKnwEybyhr7LEvBRn0RoLBABuckvB6S9gQJMEDrgU= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034/go.mod h1:DUZccDEW98n+J1mhdWGO7wr/Njad9p9Fzks839JN7Rs= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d h1:5tgMC5Gi2UAOKZ+m28W8ubjLeR0pQCAcrz6eQ0rW510= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d/go.mod h1:0UNuO3nDt9MFsZPaHJBEUolxVkN0iC69j1ccDp95e8k= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540 h1:xFSv8561jsLtF6gYZr/zW2z5qUUAkcFkApin2mnbYTo= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540/go.mod h1:sjAmX8K2kbQhvDarZE1ZZgDgmHJ50s0BBc/66vKY2ek= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917 h1:MD80ZRCTvxxJ8PBmhtrKoTnky8cVNYrCrIBLVRbrOM0= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917/go.mod h1:jwVxhctE6BgLOSSsVq9wbREpZ8Ev34H+UBxeUhESZRs= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20 h1:ybdconEoRBHLwtDKlZKYaeanQ8UoVqdDiaTlPV+qEiI= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20/go.mod h1:QqcZSwLgEIn7YraAIRmomnBMAuVFephiHrIWVlkWbFI= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0 h1:5QNKQTQpIp+0ogXUu9B85tEie473RZttOhYxM2vHyOc= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0/go.mod h1:OiWUTrrpSLLTMh7FINWjEh6mmDJCVPaC4yEsDCVaWdU= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b h1:pJQ3K0kUdOUICIOZBLAN+MaEfloG2t6Nc1+ve057pYc= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b/go.mod h1:QqcZSwLgEIn7YraAIRmomnBMAuVFephiHrIWVlkWbFI= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696 h1:h1E87+z+JcUEfvbJVF56SnZA/YUFE5ewUE61MaR/Ewg= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696/go.mod h1:OiWUTrrpSLLTMh7FINWjEh6mmDJCVPaC4yEsDCVaWdU= github.com/smartcontractkit/chainlink-testing-framework v1.28.17 h1:zezoeiG3GUGW1T2+genS/HD1BvRJwC3rqFnFTFNB9aY= github.com/smartcontractkit/chainlink-testing-framework v1.28.17/go.mod h1:xjxJK+4SUjBmJJWfRFl02poauU4XQE37aH7WYtxTLKg= github.com/smartcontractkit/chainlink-testing-framework/grafana v0.0.0-20240328204215-ac91f55f1449 h1:fX/xmGm1GBsD1ZZnooNT+eWA0hiTAqFlHzOC5CY4dy8= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index 5762df219e4..0163e4acd04 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -16,7 +16,7 @@ require ( github.com/rs/zerolog v1.30.0 github.com/slack-go/slack v0.12.2 github.com/smartcontractkit/chainlink-automation v1.0.3 - github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e + github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034 github.com/smartcontractkit/chainlink-testing-framework v1.28.17 github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20240214231432-4ad5eb95178c github.com/smartcontractkit/chainlink/v2 v2.9.0-beta0.0.20240216210048-da02459ddad8 @@ -367,8 +367,8 @@ require ( github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540 // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917 // indirect - github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20 // indirect - github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0 // indirect + github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b // indirect + github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696 // indirect github.com/smartcontractkit/chainlink-testing-framework/grafana v0.0.0-20240328204215-ac91f55f1449 // indirect github.com/smartcontractkit/chainlink-vrf v0.0.0-20231120191722-fef03814f868 // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 // indirect diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index c0ca0e3049c..1fe677a4843 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1502,18 +1502,18 @@ github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCq github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.3 h1:h/ijT0NiyV06VxYVgcNfsE3+8OEzT3Q0Z9au0z1BPWs= github.com/smartcontractkit/chainlink-automation v1.0.3/go.mod h1:RjboV0Qd7YP+To+OrzHGXaxUxoSONveCoAK2TQ1INLU= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e h1:+lMjCyABWYAEr0ueTKheYHe9YbUx27UP+zpUOi+7Jz4= -github.com/smartcontractkit/chainlink-common v0.1.7-0.20240528165711-19b9064a1d7e/go.mod h1:DUZccDEW98n+J1mhdWGO7wr/Njad9p9Fzks839JN7Rs= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034 h1:y2AKnwEybyhr7LEvBRn0RoLBABuckvB6S9gQJMEDrgU= +github.com/smartcontractkit/chainlink-common v0.1.7-0.20240603100528-9e4ad2c80034/go.mod h1:DUZccDEW98n+J1mhdWGO7wr/Njad9p9Fzks839JN7Rs= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d h1:5tgMC5Gi2UAOKZ+m28W8ubjLeR0pQCAcrz6eQ0rW510= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240524214833-c362c2ebbd2d/go.mod h1:0UNuO3nDt9MFsZPaHJBEUolxVkN0iC69j1ccDp95e8k= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540 h1:xFSv8561jsLtF6gYZr/zW2z5qUUAkcFkApin2mnbYTo= github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240220203239-09be0ea34540/go.mod h1:sjAmX8K2kbQhvDarZE1ZZgDgmHJ50s0BBc/66vKY2ek= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917 h1:MD80ZRCTvxxJ8PBmhtrKoTnky8cVNYrCrIBLVRbrOM0= github.com/smartcontractkit/chainlink-feeds v0.0.0-20240522213638-159fb2d99917/go.mod h1:jwVxhctE6BgLOSSsVq9wbREpZ8Ev34H+UBxeUhESZRs= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20 h1:ybdconEoRBHLwtDKlZKYaeanQ8UoVqdDiaTlPV+qEiI= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240524201401-88d0b3763b20/go.mod h1:QqcZSwLgEIn7YraAIRmomnBMAuVFephiHrIWVlkWbFI= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0 h1:5QNKQTQpIp+0ogXUu9B85tEie473RZttOhYxM2vHyOc= -github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240527073251-56197f1413e0/go.mod h1:OiWUTrrpSLLTMh7FINWjEh6mmDJCVPaC4yEsDCVaWdU= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b h1:pJQ3K0kUdOUICIOZBLAN+MaEfloG2t6Nc1+ve057pYc= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20240603183542-58ab0f61506b/go.mod h1:QqcZSwLgEIn7YraAIRmomnBMAuVFephiHrIWVlkWbFI= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696 h1:h1E87+z+JcUEfvbJVF56SnZA/YUFE5ewUE61MaR/Ewg= +github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20240531021326-99118e47f696/go.mod h1:OiWUTrrpSLLTMh7FINWjEh6mmDJCVPaC4yEsDCVaWdU= github.com/smartcontractkit/chainlink-testing-framework v1.28.17 h1:zezoeiG3GUGW1T2+genS/HD1BvRJwC3rqFnFTFNB9aY= github.com/smartcontractkit/chainlink-testing-framework v1.28.17/go.mod h1:xjxJK+4SUjBmJJWfRFl02poauU4XQE37aH7WYtxTLKg= github.com/smartcontractkit/chainlink-testing-framework/grafana v0.0.0-20240328204215-ac91f55f1449 h1:fX/xmGm1GBsD1ZZnooNT+eWA0hiTAqFlHzOC5CY4dy8= diff --git a/integration-tests/smoke/log_poller_test.go b/integration-tests/smoke/log_poller_test.go index 4ddffab30a6..1f77b3a6250 100644 --- a/integration-tests/smoke/log_poller_test.go +++ b/integration-tests/smoke/log_poller_test.go @@ -46,13 +46,13 @@ func TestLogPollerFewFiltersFinalityTag(t *testing.T) { // consistency test with no network disruptions with approximate emission of 1000-1100 logs per second for ~110-120 seconds // 900 filters are registered -func TestLogPollerManyFiltersFixedDepth(t *testing.T) { - t.Skip("Execute manually, when needed as it runs for a long time") +func XTestLogPollerManyFiltersFixedDepth(t *testing.T) { + t.Skip("Execute manually, when needed as it runs for a long time, remove the X from the test name to run it") executeBasicLogPollerTest(t, test_env.DefaultChainlinkNodeLogScannerSettings) } -func TestLogPollerManyFiltersFinalityTag(t *testing.T) { - t.Skip("Execute manually, when needed as it runs for a long time") +func XTestLogPollerManyFiltersFinalityTag(t *testing.T) { + t.Skip("Execute manually, when needed as it runs for a long time, remove the X from the test name to run it") executeBasicLogPollerTest(t, test_env.DefaultChainlinkNodeLogScannerSettings) } diff --git a/integration-tests/smoke/log_poller_test.go_test_list.json b/integration-tests/smoke/log_poller_test.go_test_list.json index 96939c5133b..7d94617c544 100644 --- a/integration-tests/smoke/log_poller_test.go_test_list.json +++ b/integration-tests/smoke/log_poller_test.go_test_list.json @@ -23,12 +23,6 @@ }, { "name": "TestLogPollerReplayFinalityTag" - }, - { - "name": "TestLogPollerManyFiltersFixedDepth" - }, - { - "name": "TestLogPollerManyFiltersFinalityTag" } ] } \ No newline at end of file diff --git a/testdata/scripts/node/validate/warnings.txtar b/testdata/scripts/node/validate/warnings.txtar index 3a8f97fb64b..c047b90c133 100644 --- a/testdata/scripts/node/validate/warnings.txtar +++ b/testdata/scripts/node/validate/warnings.txtar @@ -9,15 +9,6 @@ CollectorTarget = 'otel-collector:4317' TLSCertPath = 'something' Mode = 'unencrypted' -[[EVM]] -ChainID = '10200' -ChainType = 'xdai' - -[[EVM.Nodes]] -Name = 'fake' -WSURL = 'wss://foo.bar/ws' -HTTPURL = 'https://foo.bar' - -- secrets.toml -- [Database] URL = 'postgresql://user:pass1234567890abcd@localhost:5432/dbname?sslmode=disable' @@ -41,15 +32,6 @@ CollectorTarget = 'otel-collector:4317' Mode = 'unencrypted' TLSCertPath = 'something' -[[EVM]] -ChainID = '10200' -ChainType = 'xdai' - -[[EVM.Nodes]] -Name = 'fake' -WSURL = 'wss://foo.bar/ws' -HTTPURL = 'https://foo.bar' - # Effective Configuration, with defaults applied: InsecureFastScrypt = false RootDir = '~/.chainlink' @@ -302,99 +284,6 @@ DeltaDial = '15s' DeltaReconcile = '1m0s' ListenAddresses = [] -[[EVM]] -ChainID = '10200' -AutoCreateKey = true -BlockBackfillDepth = 10 -BlockBackfillSkip = false -ChainType = 'xdai' -FinalityDepth = 100 -FinalityTagEnabled = false -LogBackfillBatchSize = 1000 -LogPollInterval = '5s' -LogKeepBlocksDepth = 100000 -LogPrunePageSize = 0 -BackupLogPollerBlockDelay = 100 -MinIncomingConfirmations = 3 -MinContractPayment = '0.00001 link' -NonceAutoSync = true -NoNewHeadsThreshold = '3m0s' -RPCDefaultBatchSize = 250 -RPCBlockQueryDelay = 1 - -[EVM.Transactions] -ForwardersEnabled = false -MaxInFlight = 16 -MaxQueued = 250 -ReaperInterval = '1h0m0s' -ReaperThreshold = '168h0m0s' -ResendAfterThreshold = '1m0s' - -[EVM.Transactions.AutoPurge] -Enabled = false - -[EVM.BalanceMonitor] -Enabled = true - -[EVM.GasEstimator] -Mode = 'BlockHistory' -PriceDefault = '20 gwei' -PriceMax = '500 gwei' -PriceMin = '1 gwei' -LimitDefault = 500000 -LimitMax = 500000 -LimitMultiplier = '1' -LimitTransfer = 21000 -BumpMin = '5 gwei' -BumpPercent = 20 -BumpThreshold = 3 -EIP1559DynamicFees = true -FeeCapDefault = '100 gwei' -TipCapDefault = '1 wei' -TipCapMin = '1 wei' - -[EVM.GasEstimator.BlockHistory] -BatchSize = 25 -BlockHistorySize = 8 -CheckInclusionBlocks = 12 -CheckInclusionPercentile = 90 -TransactionPercentile = 60 - -[EVM.HeadTracker] -HistoryDepth = 100 -MaxBufferSize = 3 -SamplingInterval = '1s' -MaxAllowedFinalityDepth = 10000 -FinalityTagBypass = true - -[EVM.NodePool] -PollFailureThreshold = 5 -PollInterval = '10s' -SelectionMode = 'HighestHead' -SyncThreshold = 5 -LeaseDuration = '0s' -NodeIsSyncingEnabled = false -FinalizedBlockPollInterval = '5s' - -[EVM.OCR] -ContractConfirmations = 4 -ContractTransmitterTransmitTimeout = '10s' -DatabaseTimeout = '10s' -DeltaCOverride = '168h0m0s' -DeltaCJitterOverride = '1h0m0s' -ObservationGracePeriod = '1s' - -[EVM.OCR2] -[EVM.OCR2.Automation] -GasLimit = 5400000 - -[[EVM.Nodes]] -Name = 'fake' -WSURL = 'wss://foo.bar/ws' -HTTPURL = 'https://foo.bar' - # Configuration warning: -2 errors: - - EVM.ChainType: invalid value (xdai): deprecated and will be removed in v2.13.0, use 'gnosis' instead - - Tracing.TLSCertPath: invalid value (something): must be empty when Tracing.Mode is 'unencrypted' +Tracing.TLSCertPath: invalid value (something): must be empty when Tracing.Mode is 'unencrypted' Valid configuration. \ No newline at end of file